510 lines · cpp
1//===-- QualityTests.cpp ----------------------------------------*- C++ -*-===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8//9// Evaluating scoring functions isn't a great fit for assert-based tests.10// For interesting cases, both exact scores and "X beats Y" are too brittle to11// make good hard assertions.12//13// Here we test the signal extraction and sanity-check that signals point in14// the right direction. This should be supplemented by quality metrics which15// we can compute from a corpus of queries and preferred rankings.16//17//===----------------------------------------------------------------------===//18 19#include "FileDistance.h"20#include "Quality.h"21#include "TestFS.h"22#include "TestTU.h"23#include "index/FileIndex.h"24#include "clang/AST/Decl.h"25#include "clang/AST/DeclCXX.h"26#include "clang/Sema/CodeCompleteConsumer.h"27#include "llvm/Support/Casting.h"28#include "gmock/gmock.h"29#include "gtest/gtest.h"30#include <vector>31 32namespace clang {33namespace clangd {34 35// Force the unittest URI scheme to be linked,36[[maybe_unused]] static int UnittestSchemeAnchorDest =37 UnittestSchemeAnchorSource;38 39namespace {40 41TEST(QualityTests, SymbolQualitySignalExtraction) {42 auto Header = TestTU::withHeaderCode(R"cpp(43 int _X;44 45 [[deprecated]]46 int _f() { return _X; }47 48 #define DECL_NAME(x, y) x##_##y##_Decl49 #define DECL(x, y) class DECL_NAME(x, y) {};50 DECL(X, Y); // X_Y_Decl51 )cpp");52 53 auto Symbols = Header.headerSymbols();54 auto AST = Header.build();55 56 SymbolQualitySignals Quality;57 Quality.merge(findSymbol(Symbols, "X_Y_Decl"));58 EXPECT_TRUE(Quality.ImplementationDetail);59 60 Symbol F = findSymbol(Symbols, "_f");61 F.References = 24; // TestTU doesn't count references, so fake it.62 Quality = {};63 Quality.merge(F);64 EXPECT_TRUE(Quality.Deprecated);65 EXPECT_FALSE(Quality.ReservedName);66 EXPECT_EQ(Quality.References, 24u);67 EXPECT_EQ(Quality.Category, SymbolQualitySignals::Function);68 69 Quality = {};70 Quality.merge(CodeCompletionResult(&findDecl(AST, "_f"), /*Priority=*/42));71 EXPECT_TRUE(Quality.Deprecated);72 EXPECT_FALSE(Quality.ReservedName);73 EXPECT_EQ(Quality.References, SymbolQualitySignals().References);74 EXPECT_EQ(Quality.Category, SymbolQualitySignals::Function);75 76 Quality = {};77 Quality.merge(CodeCompletionResult("if"));78 EXPECT_EQ(Quality.Category, SymbolQualitySignals::Keyword);79 80 // Testing ReservedName in main file, we don't index those symbols in headers.81 auto MainAST = TestTU::withCode("int _X;").build();82 SymbolSlab MainSymbols = std::get<0>(indexMainDecls(MainAST));83 84 Quality = {};85 Quality.merge(findSymbol(MainSymbols, "_X"));86 EXPECT_FALSE(Quality.Deprecated);87 EXPECT_FALSE(Quality.ImplementationDetail);88 EXPECT_TRUE(Quality.ReservedName);89}90 91TEST(QualityTests, SymbolRelevanceSignalExtraction) {92 TestTU Test;93 Test.HeaderCode = R"cpp(94 int header();95 int header_main();96 97 namespace hdr { class Bar {}; } // namespace hdr98 99 #define DEFINE_FLAG(X) \100 namespace flags { \101 int FLAGS_##X; \102 } \103 104 DEFINE_FLAG(FOO)105 )cpp";106 Test.Code = R"cpp(107 using hdr::Bar;108 109 using flags::FLAGS_FOO;110 111 int ::header_main() { return 0; }112 int main();113 114 [[deprecated]]115 int deprecated() { return 0; }116 117 namespace { struct X { void y() { int z; } }; }118 struct S{};119 )cpp";120 auto AST = Test.build();121 122 SymbolRelevanceSignals Relevance;123 Relevance.merge(CodeCompletionResult(&findDecl(AST, "deprecated"),124 /*Priority=*/42,125 /*Qualifier=*/std::nullopt,126 /*QualifierIsInformative=*/false,127 /*Accessible=*/false));128 EXPECT_EQ(Relevance.NameMatch, SymbolRelevanceSignals().NameMatch);129 EXPECT_TRUE(Relevance.Forbidden);130 EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::GlobalScope);131 132 Relevance = {};133 Relevance.merge(CodeCompletionResult(&findDecl(AST, "main"), 42));134 EXPECT_FLOAT_EQ(Relevance.SemaFileProximityScore, 1.0f)135 << "Decl in current file";136 Relevance = {};137 Relevance.merge(CodeCompletionResult(&findDecl(AST, "header"), 42));138 EXPECT_FLOAT_EQ(Relevance.SemaFileProximityScore, 0.6f) << "Decl from header";139 Relevance = {};140 Relevance.merge(CodeCompletionResult(&findDecl(AST, "header_main"), 42));141 EXPECT_FLOAT_EQ(Relevance.SemaFileProximityScore, 1.0f)142 << "Current file and header";143 144 auto ConstructShadowDeclCompletionResult = [&](const std::string DeclName) {145 auto *Shadow =146 *dyn_cast<UsingDecl>(&findDecl(AST, [&](const NamedDecl &ND) {147 if (const UsingDecl *Using = dyn_cast<UsingDecl>(&ND))148 if (Using->shadow_size() &&149 Using->getQualifiedNameAsString() == DeclName)150 return true;151 return false;152 }))->shadow_begin();153 CodeCompletionResult Result(Shadow->getTargetDecl(), 42);154 Result.ShadowDecl = Shadow;155 return Result;156 };157 158 Relevance = {};159 Relevance.merge(ConstructShadowDeclCompletionResult("Bar"));160 EXPECT_FLOAT_EQ(Relevance.SemaFileProximityScore, 1.0f)161 << "Using declaration in main file";162 Relevance.merge(ConstructShadowDeclCompletionResult("FLAGS_FOO"));163 EXPECT_FLOAT_EQ(Relevance.SemaFileProximityScore, 1.0f)164 << "Using declaration in main file";165 166 Relevance = {};167 Relevance.merge(CodeCompletionResult(&findUnqualifiedDecl(AST, "X"), 42));168 EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::FileScope);169 Relevance = {};170 Relevance.merge(CodeCompletionResult(&findUnqualifiedDecl(AST, "y"), 42));171 EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::ClassScope);172 Relevance = {};173 Relevance.merge(CodeCompletionResult(&findUnqualifiedDecl(AST, "z"), 42));174 EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::FunctionScope);175 // The injected class name is treated as the outer class name.176 Relevance = {};177 Relevance.merge(CodeCompletionResult(&findDecl(AST, "S::S"), 42));178 EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::GlobalScope);179 180 Relevance = {};181 EXPECT_FALSE(Relevance.InBaseClass);182 auto BaseMember = CodeCompletionResult(&findUnqualifiedDecl(AST, "y"), 42);183 BaseMember.InBaseClass = true;184 Relevance.merge(BaseMember);185 EXPECT_TRUE(Relevance.InBaseClass);186 187 auto Index = Test.index();188 FuzzyFindRequest Req;189 Req.Query = "X";190 Req.AnyScope = true;191 bool Matched = false;192 Index->fuzzyFind(Req, [&](const Symbol &S) {193 Matched = true;194 Relevance = {};195 Relevance.merge(S);196 EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::FileScope);197 });198 EXPECT_TRUE(Matched);199}200 201// Do the signals move the scores in the direction we expect?202TEST(QualityTests, SymbolQualitySignalsSanity) {203 SymbolQualitySignals Default;204 EXPECT_EQ(Default.evaluateHeuristics(), 1);205 206 SymbolQualitySignals Deprecated;207 Deprecated.Deprecated = true;208 EXPECT_LT(Deprecated.evaluateHeuristics(), Default.evaluateHeuristics());209 210 SymbolQualitySignals ReservedName;211 ReservedName.ReservedName = true;212 EXPECT_LT(ReservedName.evaluateHeuristics(), Default.evaluateHeuristics());213 214 SymbolQualitySignals ImplementationDetail;215 ImplementationDetail.ImplementationDetail = true;216 EXPECT_LT(ImplementationDetail.evaluateHeuristics(),217 Default.evaluateHeuristics());218 219 SymbolQualitySignals WithReferences, ManyReferences;220 WithReferences.References = 20;221 ManyReferences.References = 1000;222 EXPECT_GT(WithReferences.evaluateHeuristics(), Default.evaluateHeuristics());223 EXPECT_GT(ManyReferences.evaluateHeuristics(),224 WithReferences.evaluateHeuristics());225 226 SymbolQualitySignals Keyword, Variable, Macro, Constructor, Function,227 Destructor, Operator;228 Keyword.Category = SymbolQualitySignals::Keyword;229 Variable.Category = SymbolQualitySignals::Variable;230 Macro.Category = SymbolQualitySignals::Macro;231 Constructor.Category = SymbolQualitySignals::Constructor;232 Destructor.Category = SymbolQualitySignals::Destructor;233 Destructor.Category = SymbolQualitySignals::Destructor;234 Operator.Category = SymbolQualitySignals::Operator;235 Function.Category = SymbolQualitySignals::Function;236 EXPECT_GT(Variable.evaluateHeuristics(), Default.evaluateHeuristics());237 EXPECT_GT(Keyword.evaluateHeuristics(), Variable.evaluateHeuristics());238 EXPECT_LT(Macro.evaluateHeuristics(), Default.evaluateHeuristics());239 EXPECT_LT(Operator.evaluateHeuristics(), Default.evaluateHeuristics());240 EXPECT_LT(Constructor.evaluateHeuristics(), Function.evaluateHeuristics());241 EXPECT_LT(Destructor.evaluateHeuristics(), Constructor.evaluateHeuristics());242}243 244TEST(QualityTests, SymbolRelevanceSignalsSanity) {245 SymbolRelevanceSignals Default;246 EXPECT_EQ(Default.evaluateHeuristics(), 1);247 248 SymbolRelevanceSignals Forbidden;249 Forbidden.Forbidden = true;250 EXPECT_LT(Forbidden.evaluateHeuristics(), Default.evaluateHeuristics());251 252 SymbolRelevanceSignals PoorNameMatch;253 PoorNameMatch.NameMatch = 0.2f;254 EXPECT_LT(PoorNameMatch.evaluateHeuristics(), Default.evaluateHeuristics());255 256 SymbolRelevanceSignals WithSemaFileProximity;257 WithSemaFileProximity.SemaFileProximityScore = 0.2f;258 EXPECT_GT(WithSemaFileProximity.evaluateHeuristics(),259 Default.evaluateHeuristics());260 261 ScopeDistance ScopeProximity({"x::y::"});262 263 SymbolRelevanceSignals WithSemaScopeProximity;264 WithSemaScopeProximity.ScopeProximityMatch = &ScopeProximity;265 WithSemaScopeProximity.SemaSaysInScope = true;266 EXPECT_GT(WithSemaScopeProximity.evaluateHeuristics(),267 Default.evaluateHeuristics());268 269 SymbolRelevanceSignals WithIndexScopeProximity;270 WithIndexScopeProximity.ScopeProximityMatch = &ScopeProximity;271 WithIndexScopeProximity.SymbolScope = "x::";272 EXPECT_GT(WithSemaScopeProximity.evaluateHeuristics(),273 Default.evaluateHeuristics());274 275 SymbolRelevanceSignals IndexProximate;276 IndexProximate.SymbolURI = "unittest:/foo/bar.h";277 llvm::StringMap<SourceParams> ProxSources;278 ProxSources.try_emplace(testPath("foo/baz.h"));279 URIDistance Distance(ProxSources);280 IndexProximate.FileProximityMatch = &Distance;281 EXPECT_GT(IndexProximate.evaluateHeuristics(), Default.evaluateHeuristics());282 SymbolRelevanceSignals IndexDistant = IndexProximate;283 IndexDistant.SymbolURI = "unittest:/elsewhere/path.h";284 EXPECT_GT(IndexProximate.evaluateHeuristics(),285 IndexDistant.evaluateHeuristics())286 << IndexProximate << IndexDistant;287 EXPECT_GT(IndexDistant.evaluateHeuristics(), Default.evaluateHeuristics());288 289 SymbolRelevanceSignals Scoped;290 Scoped.Scope = SymbolRelevanceSignals::FileScope;291 EXPECT_LT(Scoped.evaluateHeuristics(), Default.evaluateHeuristics());292 Scoped.Query = SymbolRelevanceSignals::CodeComplete;293 EXPECT_GT(Scoped.evaluateHeuristics(), Default.evaluateHeuristics());294 295 SymbolRelevanceSignals Instance;296 Instance.IsInstanceMember = false;297 EXPECT_EQ(Instance.evaluateHeuristics(), Default.evaluateHeuristics());298 Instance.Context = CodeCompletionContext::CCC_DotMemberAccess;299 EXPECT_LT(Instance.evaluateHeuristics(), Default.evaluateHeuristics());300 Instance.IsInstanceMember = true;301 EXPECT_EQ(Instance.evaluateHeuristics(), Default.evaluateHeuristics());302 303 SymbolRelevanceSignals InBaseClass;304 InBaseClass.InBaseClass = true;305 EXPECT_LT(InBaseClass.evaluateHeuristics(), Default.evaluateHeuristics());306 307 llvm::StringSet<> Words = {"one", "two", "three"};308 SymbolRelevanceSignals WithoutMatchingWord;309 WithoutMatchingWord.ContextWords = &Words;310 WithoutMatchingWord.Name = "four";311 EXPECT_EQ(WithoutMatchingWord.evaluateHeuristics(),312 Default.evaluateHeuristics());313 SymbolRelevanceSignals WithMatchingWord;314 WithMatchingWord.ContextWords = &Words;315 WithMatchingWord.Name = "TheTwoTowers";316 EXPECT_GT(WithMatchingWord.evaluateHeuristics(),317 Default.evaluateHeuristics());318}319 320TEST(QualityTests, ScopeProximity) {321 SymbolRelevanceSignals Relevance;322 ScopeDistance ScopeProximity({"x::y::z::", "x::", "llvm::", ""});323 Relevance.ScopeProximityMatch = &ScopeProximity;324 325 Relevance.SymbolScope = "other::";326 float NotMatched = Relevance.evaluateHeuristics();327 328 Relevance.SymbolScope = "";329 float Global = Relevance.evaluateHeuristics();330 EXPECT_GT(Global, NotMatched);331 332 Relevance.SymbolScope = "llvm::";333 float NonParent = Relevance.evaluateHeuristics();334 EXPECT_GT(NonParent, Global);335 336 Relevance.SymbolScope = "x::";337 float GrandParent = Relevance.evaluateHeuristics();338 EXPECT_GT(GrandParent, Global);339 340 Relevance.SymbolScope = "x::y::";341 float Parent = Relevance.evaluateHeuristics();342 EXPECT_GT(Parent, GrandParent);343 344 Relevance.SymbolScope = "x::y::z::";345 float Enclosing = Relevance.evaluateHeuristics();346 EXPECT_GT(Enclosing, Parent);347}348 349TEST(QualityTests, SortText) {350 EXPECT_LT(sortText(std::numeric_limits<float>::infinity()),351 sortText(1000.2f));352 EXPECT_LT(sortText(1000.2f), sortText(1));353 EXPECT_LT(sortText(1), sortText(0.3f));354 EXPECT_LT(sortText(0.3f), sortText(0));355 EXPECT_LT(sortText(0), sortText(-10));356 EXPECT_LT(sortText(-10), sortText(-std::numeric_limits<float>::infinity()));357 358 EXPECT_LT(sortText(1, "z"), sortText(0, "a"));359 EXPECT_LT(sortText(0, "a"), sortText(0, "z"));360}361 362TEST(QualityTests, NoBoostForClassConstructor) {363 auto Header = TestTU::withHeaderCode(R"cpp(364 class Foo {365 public:366 Foo(int);367 };368 )cpp");369 auto Symbols = Header.headerSymbols();370 auto AST = Header.build();371 372 const NamedDecl *Foo = &findDecl(AST, "Foo");373 SymbolRelevanceSignals Cls;374 Cls.merge(CodeCompletionResult(Foo, /*Priority=*/0));375 376 const NamedDecl *CtorDecl = &findDecl(AST, [](const NamedDecl &ND) {377 return (ND.getQualifiedNameAsString() == "Foo::Foo") &&378 isa<CXXConstructorDecl>(&ND);379 });380 SymbolRelevanceSignals Ctor;381 Ctor.merge(CodeCompletionResult(CtorDecl, /*Priority=*/0));382 383 EXPECT_EQ(Cls.Scope, SymbolRelevanceSignals::GlobalScope);384 EXPECT_EQ(Ctor.Scope, SymbolRelevanceSignals::GlobalScope);385}386 387TEST(QualityTests, IsInstanceMember) {388 auto Header = TestTU::withHeaderCode(R"cpp(389 class Foo {390 public:391 static void foo() {}392 393 template <typename T> void tpl(T *t) {}394 395 void bar() {}396 };397 )cpp");398 auto Symbols = Header.headerSymbols();399 400 SymbolRelevanceSignals Rel;401 const Symbol &FooSym = findSymbol(Symbols, "Foo::foo");402 Rel.merge(FooSym);403 EXPECT_FALSE(Rel.IsInstanceMember);404 const Symbol &BarSym = findSymbol(Symbols, "Foo::bar");405 Rel.merge(BarSym);406 EXPECT_TRUE(Rel.IsInstanceMember);407 408 Rel.IsInstanceMember = false;409 const Symbol &TplSym = findSymbol(Symbols, "Foo::tpl");410 Rel.merge(TplSym);411 EXPECT_TRUE(Rel.IsInstanceMember);412 413 auto AST = Header.build();414 const NamedDecl *Foo = &findDecl(AST, "Foo::foo");415 const NamedDecl *Bar = &findDecl(AST, "Foo::bar");416 const NamedDecl *Tpl = &findDecl(AST, "Foo::tpl");417 418 Rel.IsInstanceMember = false;419 Rel.merge(CodeCompletionResult(Foo, /*Priority=*/0));420 EXPECT_FALSE(Rel.IsInstanceMember);421 Rel.merge(CodeCompletionResult(Bar, /*Priority=*/0));422 EXPECT_TRUE(Rel.IsInstanceMember);423 Rel.IsInstanceMember = false;424 Rel.merge(CodeCompletionResult(Tpl, /*Priority=*/0));425 EXPECT_TRUE(Rel.IsInstanceMember);426}427 428TEST(QualityTests, ConstructorDestructor) {429 auto Header = TestTU::withHeaderCode(R"cpp(430 class Foo {431 public:432 Foo(int);433 ~Foo();434 };435 )cpp");436 auto Symbols = Header.headerSymbols();437 auto AST = Header.build();438 439 const NamedDecl *CtorDecl = &findDecl(AST, [](const NamedDecl &ND) {440 return (ND.getQualifiedNameAsString() == "Foo::Foo") &&441 isa<CXXConstructorDecl>(&ND);442 });443 const NamedDecl *DtorDecl = &findDecl(AST, [](const NamedDecl &ND) {444 return (ND.getQualifiedNameAsString() == "Foo::~Foo") &&445 isa<CXXDestructorDecl>(&ND);446 });447 448 SymbolQualitySignals CtorQ;449 CtorQ.merge(CodeCompletionResult(CtorDecl, /*Priority=*/0));450 EXPECT_EQ(CtorQ.Category, SymbolQualitySignals::Constructor);451 452 CtorQ.Category = SymbolQualitySignals::Unknown;453 const Symbol &CtorSym = findSymbol(Symbols, "Foo::Foo");454 CtorQ.merge(CtorSym);455 EXPECT_EQ(CtorQ.Category, SymbolQualitySignals::Constructor);456 457 SymbolQualitySignals DtorQ;458 DtorQ.merge(CodeCompletionResult(DtorDecl, /*Priority=*/0));459 EXPECT_EQ(DtorQ.Category, SymbolQualitySignals::Destructor);460}461 462TEST(QualityTests, Operator) {463 auto Header = TestTU::withHeaderCode(R"cpp(464 class Foo {465 public:466 bool operator<(const Foo& f1);467 };468 )cpp");469 auto AST = Header.build();470 471 const NamedDecl *Operator = &findDecl(AST, [](const NamedDecl &ND) {472 if (const auto *OD = dyn_cast<FunctionDecl>(&ND))473 if (OD->isOverloadedOperator())474 return true;475 return false;476 });477 SymbolQualitySignals Q;478 Q.merge(CodeCompletionResult(Operator, /*Priority=*/0));479 EXPECT_EQ(Q.Category, SymbolQualitySignals::Operator);480}481 482TEST(QualityTests, ItemWithFixItsRankedDown) {483 CodeCompleteOptions Opts;484 Opts.IncludeFixIts = true;485 486 auto Header = TestTU::withHeaderCode(R"cpp(487 int x;488 )cpp");489 auto AST = Header.build();490 491 SymbolRelevanceSignals RelevanceWithFixIt;492 RelevanceWithFixIt.merge(CodeCompletionResult(493 &findDecl(AST, "x"), /*Priority=*/0, /*Qualifier=*/std::nullopt,494 /*QualifierIsInformative=*/false, /*Accessible=*/true, {FixItHint{}}));495 EXPECT_TRUE(RelevanceWithFixIt.NeedsFixIts);496 497 SymbolRelevanceSignals RelevanceWithoutFixIt;498 RelevanceWithoutFixIt.merge(CodeCompletionResult(499 &findDecl(AST, "x"), /*Priority=*/0, /*Qualifier=*/std::nullopt,500 /*QualifierIsInformative=*/false, /*Accessible=*/true, {}));501 EXPECT_FALSE(RelevanceWithoutFixIt.NeedsFixIts);502 503 EXPECT_LT(RelevanceWithFixIt.evaluateHeuristics(),504 RelevanceWithoutFixIt.evaluateHeuristics());505}506 507} // namespace508} // namespace clangd509} // namespace clang510