llvm-project/clang-tools-extra/clangd/unittests/ParsedASTTests.cpp

641 lines
21 KiB
C++

//===-- ParsedASTTests.cpp ------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// These tests cover clangd's logic to build a TU, which generally uses the APIs
// in ParsedAST and Preamble, via the TestTU helper.
//
//===----------------------------------------------------------------------===//
#include "../../clang-tidy/ClangTidyCheck.h"
#include "../../clang-tidy/ClangTidyModule.h"
#include "../../clang-tidy/ClangTidyModuleRegistry.h"
#include "AST.h"
#include "Annotations.h"
#include "Compiler.h"
#include "Diagnostics.h"
#include "Headers.h"
#include "ParsedAST.h"
#include "Preamble.h"
#include "SourceCode.h"
#include "TestFS.h"
#include "TestTU.h"
#include "clang/AST/DeclTemplate.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Basic/TokenKinds.h"
#include "clang/Lex/PPCallbacks.h"
#include "clang/Lex/Token.h"
#include "clang/Tooling/Syntax/Tokens.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/ScopedPrinter.h"
#include "gmock/gmock-matchers.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace clang {
namespace clangd {
namespace {
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
MATCHER_P(DeclNamed, Name, "") {
if (NamedDecl *ND = dyn_cast<NamedDecl>(arg))
if (ND->getName() == Name)
return true;
if (auto *Stream = result_listener->stream()) {
llvm::raw_os_ostream OS(*Stream);
arg->dump(OS);
}
return false;
}
MATCHER_P(DeclKind, Kind, "") {
if (NamedDecl *ND = dyn_cast<NamedDecl>(arg))
if (ND->getDeclKindName() == llvm::StringRef(Kind))
return true;
if (auto *Stream = result_listener->stream()) {
llvm::raw_os_ostream OS(*Stream);
arg->dump(OS);
}
return false;
}
// Matches if the Decl has template args equal to ArgName. If the decl is a
// NamedDecl and ArgName is an empty string it also matches.
MATCHER_P(WithTemplateArgs, ArgName, "") {
if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(arg)) {
if (const auto *Args = FD->getTemplateSpecializationArgs()) {
std::string SpecializationArgs;
// Without the PrintingPolicy "bool" will be printed as "_Bool".
LangOptions LO;
PrintingPolicy Policy(LO);
Policy.adjustForCPlusPlus();
for (const auto &Arg : Args->asArray()) {
if (SpecializationArgs.size() > 0)
SpecializationArgs += ",";
SpecializationArgs += Arg.getAsType().getAsString(Policy);
}
if (Args->size() == 0)
return ArgName == SpecializationArgs;
return ArgName == "<" + SpecializationArgs + ">";
}
}
if (const NamedDecl *ND = dyn_cast<NamedDecl>(arg))
return printTemplateSpecializationArgs(*ND) == ArgName;
return false;
}
MATCHER_P(RangeIs, R, "") {
return arg.beginOffset() == R.Begin && arg.endOffset() == R.End;
}
MATCHER(EqInc, "") {
Inclusion Actual = testing::get<0>(arg);
Inclusion Expected = testing::get<1>(arg);
return std::tie(Actual.HashLine, Actual.Written) ==
std::tie(Expected.HashLine, Expected.Written);
}
TEST(ParsedASTTest, TopLevelDecls) {
TestTU TU;
TU.HeaderCode = R"(
int header1();
int header2;
)";
TU.Code = R"cpp(
int main();
template <typename> bool X = true;
)cpp";
auto AST = TU.build();
EXPECT_THAT(AST.getLocalTopLevelDecls(),
testing::UnorderedElementsAreArray(
{AllOf(DeclNamed("main"), DeclKind("Function")),
AllOf(DeclNamed("X"), DeclKind("VarTemplate"))}));
}
TEST(ParsedASTTest, DoesNotGetIncludedTopDecls) {
TestTU TU;
TU.HeaderCode = R"cpp(
#define LL void foo(){}
template<class T>
struct H {
H() {}
LL
};
)cpp";
TU.Code = R"cpp(
int main() {
H<int> h;
h.foo();
}
)cpp";
auto AST = TU.build();
EXPECT_THAT(AST.getLocalTopLevelDecls(), ElementsAre(DeclNamed("main")));
}
TEST(ParsedASTTest, DoesNotGetImplicitTemplateTopDecls) {
TestTU TU;
TU.Code = R"cpp(
template<typename T>
void f(T) {}
void s() {
f(10UL);
}
)cpp";
auto AST = TU.build();
EXPECT_THAT(AST.getLocalTopLevelDecls(),
ElementsAre(DeclNamed("f"), DeclNamed("s")));
}
TEST(ParsedASTTest,
GetsExplicitInstantiationAndSpecializationTemplateTopDecls) {
TestTU TU;
TU.Code = R"cpp(
template <typename T>
void f(T) {}
template<>
void f(bool);
template void f(double);
template <class T>
struct V {};
template<class T>
struct V<T*> {};
template <>
struct V<bool> {};
template<class T>
T foo = T(10);
int i = foo<int>;
double d = foo<double>;
template <class T>
int foo<T*> = 0;
template <>
int foo<bool> = 0;
)cpp";
// FIXME: Auto-completion in a template requires disabling delayed template
// parsing.
TU.ExtraArgs.push_back("-fno-delayed-template-parsing");
auto AST = TU.build();
EXPECT_THAT(
AST.getLocalTopLevelDecls(),
ElementsAreArray({AllOf(DeclNamed("f"), WithTemplateArgs("")),
AllOf(DeclNamed("f"), WithTemplateArgs("<bool>")),
AllOf(DeclNamed("f"), WithTemplateArgs("<double>")),
AllOf(DeclNamed("V"), WithTemplateArgs("")),
AllOf(DeclNamed("V"), WithTemplateArgs("<T *>")),
AllOf(DeclNamed("V"), WithTemplateArgs("<bool>")),
AllOf(DeclNamed("foo"), WithTemplateArgs("")),
AllOf(DeclNamed("i"), WithTemplateArgs("")),
AllOf(DeclNamed("d"), WithTemplateArgs("")),
AllOf(DeclNamed("foo"), WithTemplateArgs("<T *>")),
AllOf(DeclNamed("foo"), WithTemplateArgs("<bool>"))}));
}
TEST(ParsedASTTest, IgnoresDelayedTemplateParsing) {
auto TU = TestTU::withCode(R"cpp(
template <typename T> void xxx() {
int yyy = 0;
}
)cpp");
TU.ExtraArgs.push_back("-fdelayed-template-parsing");
auto AST = TU.build();
EXPECT_EQ(Decl::Var, findUnqualifiedDecl(AST, "yyy").getKind());
}
TEST(ParsedASTTest, TokensAfterPreamble) {
TestTU TU;
TU.AdditionalFiles["foo.h"] = R"(
int foo();
)";
TU.Code = R"cpp(
#include "foo.h"
first_token;
void test() {
// error-ok: invalid syntax, just examining token stream
}
last_token
)cpp";
auto AST = TU.build();
const syntax::TokenBuffer &T = AST.getTokens();
const auto &SM = AST.getSourceManager();
ASSERT_GT(T.expandedTokens().size(), 2u);
// Check first token after the preamble.
EXPECT_EQ(T.expandedTokens().front().text(SM), "first_token");
// Last token is always 'eof'.
EXPECT_EQ(T.expandedTokens().back().kind(), tok::eof);
// Check the token before 'eof'.
EXPECT_EQ(T.expandedTokens().drop_back().back().text(SM), "last_token");
// The spelled tokens for the main file should have everything.
auto Spelled = T.spelledTokens(SM.getMainFileID());
ASSERT_FALSE(Spelled.empty());
EXPECT_EQ(Spelled.front().kind(), tok::hash);
EXPECT_EQ(Spelled.back().text(SM), "last_token");
}
TEST(ParsedASTTest, NoCrashOnTokensWithTidyCheck) {
TestTU TU;
// this check runs the preprocessor, we need to make sure it does not break
// our recording logic.
TU.ClangTidyChecks = "modernize-use-trailing-return-type";
TU.Code = "inline int foo() {}";
auto AST = TU.build();
const syntax::TokenBuffer &T = AST.getTokens();
const auto &SM = AST.getSourceManager();
ASSERT_GT(T.expandedTokens().size(), 7u);
// Check first token after the preamble.
EXPECT_EQ(T.expandedTokens().front().text(SM), "inline");
// Last token is always 'eof'.
EXPECT_EQ(T.expandedTokens().back().kind(), tok::eof);
// Check the token before 'eof'.
EXPECT_EQ(T.expandedTokens().drop_back().back().text(SM), "}");
}
TEST(ParsedASTTest, CanBuildInvocationWithUnknownArgs) {
MockFS FS;
FS.Files = {{testPath("foo.cpp"), "void test() {}"}};
// Unknown flags should not prevent a build of compiler invocation.
ParseInputs Inputs;
Inputs.TFS = &FS;
Inputs.CompileCommand.CommandLine = {"clang", "-fsome-unknown-flag",
testPath("foo.cpp")};
IgnoreDiagnostics IgnoreDiags;
EXPECT_NE(buildCompilerInvocation(Inputs, IgnoreDiags), nullptr);
// Unknown forwarded to -cc1 should not a failure either.
Inputs.CompileCommand.CommandLine = {
"clang", "-Xclang", "-fsome-unknown-flag", testPath("foo.cpp")};
EXPECT_NE(buildCompilerInvocation(Inputs, IgnoreDiags), nullptr);
}
TEST(ParsedASTTest, CollectsMainFileMacroExpansions) {
Annotations TestCase(R"cpp(
#define ^MACRO_ARGS(X, Y) X Y
// - preamble ends
^ID(int A);
// Macro arguments included.
^MACRO_ARGS(^MACRO_ARGS(^MACRO_EXP(int), E), ^ID(= 2));
// Macro names inside other macros not included.
#define ^MACRO_ARGS2(X, Y) X Y
#define ^FOO BAR
#define ^BAR 1
int F = ^FOO;
// Macros from token concatenations not included.
#define ^CONCAT(X) X##A()
#define ^PREPEND(X) MACRO##X()
#define ^MACROA() 123
int G = ^CONCAT(MACRO);
int H = ^PREPEND(A);
// Macros included not from preamble not included.
#include "foo.inc"
int printf(const char*, ...);
void exit(int);
#define ^assert(COND) if (!(COND)) { printf("%s", #COND); exit(0); }
void test() {
// Includes macro expansions in arguments that are expressions
^assert(0 <= ^BAR);
}
#ifdef ^UNDEFINED
#endif
#define ^MULTIPLE_DEFINITION 1
#undef ^MULTIPLE_DEFINITION
#define ^MULTIPLE_DEFINITION 2
#undef ^MULTIPLE_DEFINITION
)cpp");
auto TU = TestTU::withCode(TestCase.code());
TU.HeaderCode = R"cpp(
#define ID(X) X
#define MACRO_EXP(X) ID(X)
MACRO_EXP(int B);
)cpp";
TU.AdditionalFiles["foo.inc"] = R"cpp(
int C = ID(1);
#define DEF 1
int D = DEF;
)cpp";
ParsedAST AST = TU.build();
std::vector<Position> MacroExpansionPositions;
for (const auto &SIDToRefs : AST.getMacros().MacroRefs) {
for (const auto &R : SIDToRefs.second)
MacroExpansionPositions.push_back(R.start);
}
for (const auto &R : AST.getMacros().UnknownMacros)
MacroExpansionPositions.push_back(R.start);
EXPECT_THAT(MacroExpansionPositions,
testing::UnorderedElementsAreArray(TestCase.points()));
}
MATCHER_P(WithFileName, Inc, "") { return arg.FileName == Inc; }
TEST(ParsedASTTest, ReplayPreambleForTidyCheckers) {
struct Inclusion {
Inclusion(const SourceManager &SM, SourceLocation HashLoc,
const Token &IncludeTok, llvm::StringRef FileName, bool IsAngled,
CharSourceRange FilenameRange)
: HashOffset(SM.getDecomposedLoc(HashLoc).second), IncTok(IncludeTok),
IncDirective(IncludeTok.getIdentifierInfo()->getName()),
FileNameOffset(SM.getDecomposedLoc(FilenameRange.getBegin()).second),
FileName(FileName), IsAngled(IsAngled) {}
size_t HashOffset;
syntax::Token IncTok;
llvm::StringRef IncDirective;
size_t FileNameOffset;
llvm::StringRef FileName;
bool IsAngled;
};
static std::vector<Inclusion> Includes;
static std::vector<syntax::Token> SkippedFiles;
struct ReplayPreamblePPCallback : public PPCallbacks {
const SourceManager &SM;
explicit ReplayPreamblePPCallback(const SourceManager &SM) : SM(SM) {}
void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok,
StringRef FileName, bool IsAngled,
CharSourceRange FilenameRange, const FileEntry *,
StringRef, StringRef, const Module *,
SrcMgr::CharacteristicKind) override {
Includes.emplace_back(SM, HashLoc, IncludeTok, FileName, IsAngled,
FilenameRange);
}
void FileSkipped(const FileEntryRef &, const Token &FilenameTok,
SrcMgr::CharacteristicKind) override {
SkippedFiles.emplace_back(FilenameTok);
}
};
struct ReplayPreambleCheck : public tidy::ClangTidyCheck {
ReplayPreambleCheck(StringRef Name, tidy::ClangTidyContext *Context)
: ClangTidyCheck(Name, Context) {}
void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP,
Preprocessor *ModuleExpanderPP) override {
PP->addPPCallbacks(::std::make_unique<ReplayPreamblePPCallback>(SM));
}
};
struct ReplayPreambleModule : public tidy::ClangTidyModule {
void
addCheckFactories(tidy::ClangTidyCheckFactories &CheckFactories) override {
CheckFactories.registerCheck<ReplayPreambleCheck>(
"replay-preamble-check");
}
};
static tidy::ClangTidyModuleRegistry::Add<ReplayPreambleModule> X(
"replay-preamble-module", "");
TestTU TU;
// This check records inclusion directives replayed by clangd.
TU.ClangTidyChecks = "replay-preamble-check";
llvm::Annotations Test(R"cpp(
$hash^#$include[[import]] $filebegin^"$filerange[[bar.h]]"
$hash^#$include[[include_next]] $filebegin^"$filerange[[baz.h]]"
$hash^#$include[[include]] $filebegin^<$filerange[[a.h]]>)cpp");
llvm::StringRef Code = Test.code();
TU.Code = Code.str();
TU.AdditionalFiles["bar.h"] = "";
TU.AdditionalFiles["baz.h"] = "";
TU.AdditionalFiles["a.h"] = "";
// Since we are also testing #import directives, and they don't make much
// sense in c++ (also they actually break on windows), just set language to
// obj-c.
TU.ExtraArgs = {"-isystem.", "-xobjective-c"};
const auto &AST = TU.build();
const auto &SM = AST.getSourceManager();
auto HashLocs = Test.points("hash");
ASSERT_EQ(HashLocs.size(), Includes.size());
auto IncludeRanges = Test.ranges("include");
ASSERT_EQ(IncludeRanges.size(), Includes.size());
auto FileBeginLocs = Test.points("filebegin");
ASSERT_EQ(FileBeginLocs.size(), Includes.size());
auto FileRanges = Test.ranges("filerange");
ASSERT_EQ(FileRanges.size(), Includes.size());
ASSERT_EQ(SkippedFiles.size(), Includes.size());
for (size_t I = 0; I < Includes.size(); ++I) {
const auto &Inc = Includes[I];
EXPECT_EQ(Inc.HashOffset, HashLocs[I]);
auto IncRange = IncludeRanges[I];
EXPECT_THAT(Inc.IncTok.range(SM), RangeIs(IncRange));
EXPECT_EQ(Inc.IncTok.kind(), tok::identifier);
EXPECT_EQ(Inc.IncDirective,
Code.substr(IncRange.Begin, IncRange.End - IncRange.Begin));
EXPECT_EQ(Inc.FileNameOffset, FileBeginLocs[I]);
EXPECT_EQ(Inc.IsAngled, Code[FileBeginLocs[I]] == '<');
auto FileRange = FileRanges[I];
EXPECT_EQ(Inc.FileName,
Code.substr(FileRange.Begin, FileRange.End - FileRange.Begin));
EXPECT_EQ(SM.getDecomposedLoc(SkippedFiles[I].location()).second,
Inc.FileNameOffset);
// This also contains quotes/angles so increment the range by one from both
// sides.
EXPECT_EQ(
SkippedFiles[I].text(SM),
Code.substr(FileRange.Begin - 1, FileRange.End - FileRange.Begin + 2));
EXPECT_EQ(SkippedFiles[I].kind(), tok::header_name);
}
TU.AdditionalFiles["a.h"] = "";
TU.AdditionalFiles["b.h"] = "";
TU.AdditionalFiles["c.h"] = "";
// Make sure replay logic works with patched preambles.
llvm::StringLiteral Baseline = R"cpp(
#include "a.h"
#include "c.h")cpp";
MockFS FS;
TU.Code = Baseline.str();
auto Inputs = TU.inputs(FS);
auto BaselinePreamble = TU.preamble();
ASSERT_TRUE(BaselinePreamble);
// First make sure we don't crash on various modifications to the preamble.
llvm::StringLiteral Cases[] = {
// clang-format off
// New include in middle.
R"cpp(
#include "a.h"
#include "b.h"
#include "c.h")cpp",
// New include at top.
R"cpp(
#include "b.h"
#include "a.h"
#include "c.h")cpp",
// New include at bottom.
R"cpp(
#include "a.h"
#include "c.h"
#include "b.h")cpp",
// Same size with a missing include.
R"cpp(
#include "a.h"
#include "b.h")cpp",
// Smaller with no new includes.
R"cpp(
#include "a.h")cpp",
// Smaller with a new includes.
R"cpp(
#include "b.h")cpp",
// clang-format on
};
for (llvm::StringLiteral Case : Cases) {
TU.Code = Case.str();
IgnoreDiagnostics Diags;
auto CI = buildCompilerInvocation(TU.inputs(FS), Diags);
auto PatchedAST = ParsedAST::build(testPath(TU.Filename), TU.inputs(FS),
std::move(CI), {}, BaselinePreamble);
ASSERT_TRUE(PatchedAST);
EXPECT_TRUE(PatchedAST->getDiagnostics().empty());
}
// Then ensure correctness by making sure includes were seen only once.
// Note that we first see the includes from the patch, as preamble includes
// are replayed after exiting the built-in file.
Includes.clear();
TU.Code = R"cpp(
#include "a.h"
#include "b.h")cpp";
IgnoreDiagnostics Diags;
auto CI = buildCompilerInvocation(TU.inputs(FS), Diags);
auto PatchedAST = ParsedAST::build(testPath(TU.Filename), TU.inputs(FS),
std::move(CI), {}, BaselinePreamble);
ASSERT_TRUE(PatchedAST);
EXPECT_TRUE(PatchedAST->getDiagnostics().empty());
EXPECT_THAT(Includes,
ElementsAre(WithFileName(testPath("__preamble_patch__.h")),
WithFileName("b.h"), WithFileName("a.h")));
}
TEST(ParsedASTTest, PatchesAdditionalIncludes) {
llvm::StringLiteral ModifiedContents = R"cpp(
#include "baz.h"
#include "foo.h"
#include "sub/aux.h"
void bar() {
foo();
baz();
aux();
})cpp";
// Build expected ast with symbols coming from headers.
TestTU TU;
TU.Filename = "foo.cpp";
TU.AdditionalFiles["foo.h"] = "void foo();";
TU.AdditionalFiles["sub/baz.h"] = "void baz();";
TU.AdditionalFiles["sub/aux.h"] = "void aux();";
TU.ExtraArgs = {"-I" + testPath("sub")};
TU.Code = ModifiedContents.str();
auto ExpectedAST = TU.build();
// Build preamble with no includes.
TU.Code = "";
StoreDiags Diags;
MockFS FS;
auto Inputs = TU.inputs(FS);
auto CI = buildCompilerInvocation(Inputs, Diags);
auto EmptyPreamble =
buildPreamble(testPath("foo.cpp"), *CI, Inputs, true, nullptr);
ASSERT_TRUE(EmptyPreamble);
EXPECT_THAT(EmptyPreamble->Includes.MainFileIncludes, testing::IsEmpty());
// Now build an AST using empty preamble and ensure patched includes worked.
TU.Code = ModifiedContents.str();
Inputs = TU.inputs(FS);
auto PatchedAST = ParsedAST::build(testPath("foo.cpp"), Inputs, std::move(CI),
{}, EmptyPreamble);
ASSERT_TRUE(PatchedAST);
ASSERT_TRUE(PatchedAST->getDiagnostics().empty());
// Ensure source location information is correct, including resolved paths.
EXPECT_THAT(PatchedAST->getIncludeStructure().MainFileIncludes,
testing::Pointwise(
EqInc(), ExpectedAST.getIncludeStructure().MainFileIncludes));
auto StringMapToVector = [](const llvm::StringMap<unsigned> SM) {
std::vector<std::pair<std::string, unsigned>> Res;
for (const auto &E : SM)
Res.push_back({E.first().str(), E.second});
llvm::sort(Res);
return Res;
};
// Ensure file proximity signals are correct.
EXPECT_EQ(StringMapToVector(PatchedAST->getIncludeStructure().includeDepth(
testPath("foo.cpp"))),
StringMapToVector(ExpectedAST.getIncludeStructure().includeDepth(
testPath("foo.cpp"))));
}
TEST(ParsedASTTest, PatchesDeletedIncludes) {
TestTU TU;
TU.Filename = "foo.cpp";
TU.Code = "";
auto ExpectedAST = TU.build();
// Build preamble with no includes.
TU.Code = R"cpp(#include <foo.h>)cpp";
StoreDiags Diags;
MockFS FS;
auto Inputs = TU.inputs(FS);
auto CI = buildCompilerInvocation(Inputs, Diags);
auto BaselinePreamble =
buildPreamble(testPath("foo.cpp"), *CI, Inputs, true, nullptr);
ASSERT_TRUE(BaselinePreamble);
EXPECT_THAT(BaselinePreamble->Includes.MainFileIncludes,
ElementsAre(testing::Field(&Inclusion::Written, "<foo.h>")));
// Now build an AST using additional includes and check that locations are
// correctly parsed.
TU.Code = "";
Inputs = TU.inputs(FS);
auto PatchedAST = ParsedAST::build(testPath("foo.cpp"), Inputs, std::move(CI),
{}, BaselinePreamble);
ASSERT_TRUE(PatchedAST);
// Ensure source location information is correct.
EXPECT_THAT(PatchedAST->getIncludeStructure().MainFileIncludes,
testing::Pointwise(
EqInc(), ExpectedAST.getIncludeStructure().MainFileIncludes));
auto StringMapToVector = [](const llvm::StringMap<unsigned> SM) {
std::vector<std::pair<std::string, unsigned>> Res;
for (const auto &E : SM)
Res.push_back({E.first().str(), E.second});
llvm::sort(Res);
return Res;
};
// Ensure file proximity signals are correct.
EXPECT_EQ(StringMapToVector(PatchedAST->getIncludeStructure().includeDepth(
testPath("foo.cpp"))),
StringMapToVector(ExpectedAST.getIncludeStructure().includeDepth(
testPath("foo.cpp"))));
}
} // namespace
} // namespace clangd
} // namespace clang