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

552 lines
16 KiB
C++

//===--- PreambleTests.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 "Annotations.h"
#include "Compiler.h"
#include "Headers.h"
#include "Hover.h"
#include "Preamble.h"
#include "SourceCode.h"
#include "TestFS.h"
#include "TestTU.h"
#include "XRefs.h"
#include "clang/Format/Format.h"
#include "clang/Frontend/PrecompiledPreamble.h"
#include "clang/Lex/PreprocessorOptions.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/VirtualFileSystem.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <clang/Frontend/FrontendActions.h>
#include <memory>
#include <string>
#include <vector>
using testing::Contains;
using testing::Field;
using testing::Matcher;
using testing::MatchesRegex;
namespace clang {
namespace clangd {
namespace {
MATCHER_P2(Distance, File, D, "") {
return arg.first() == File && arg.second == D;
}
// Builds a preamble for BaselineContents, patches it for ModifiedContents and
// returns the includes in the patch.
IncludeStructure
collectPatchedIncludes(llvm::StringRef ModifiedContents,
llvm::StringRef BaselineContents,
llvm::StringRef MainFileName = "main.cpp") {
MockFS FS;
auto TU = TestTU::withCode(BaselineContents);
TU.Filename = MainFileName.str();
// ms-compatibility changes meaning of #import, make sure it is turned off.
TU.ExtraArgs = {"-fno-ms-compatibility"};
auto BaselinePreamble = TU.preamble();
// Create the patch.
TU.Code = ModifiedContents.str();
auto PI = TU.inputs(FS);
auto PP = PreamblePatch::create(testPath(TU.Filename), PI, *BaselinePreamble);
// Collect patch contents.
IgnoreDiagnostics Diags;
auto CI = buildCompilerInvocation(PI, Diags);
PP.apply(*CI);
// Run preprocessor over the modified contents with patched Invocation. We
// provide a preamble and trim contents to ensure only the implicit header
// introduced by the patch is parsed and nothing else.
// We don't run PP directly over the patch cotents to test production
// behaviour.
auto Bounds = Lexer::ComputePreamble(ModifiedContents, *CI->getLangOpts());
auto Clang =
prepareCompilerInstance(std::move(CI), &BaselinePreamble->Preamble,
llvm::MemoryBuffer::getMemBufferCopy(
ModifiedContents.slice(0, Bounds.Size).str()),
PI.TFS->view(PI.CompileCommand.Directory), Diags);
PreprocessOnlyAction Action;
if (!Action.BeginSourceFile(*Clang, Clang->getFrontendOpts().Inputs[0])) {
ADD_FAILURE() << "failed begin source file";
return {};
}
IncludeStructure Includes;
Clang->getPreprocessor().addPPCallbacks(
collectIncludeStructureCallback(Clang->getSourceManager(), &Includes));
if (llvm::Error Err = Action.Execute()) {
ADD_FAILURE() << "failed to execute action: " << std::move(Err);
return {};
}
Action.EndSourceFile();
return Includes;
}
// Check preamble lexing logic by building an empty preamble and patching it
// with all the contents.
TEST(PreamblePatchTest, IncludeParsing) {
// We expect any line with a point to show up in the patch.
llvm::StringRef Cases[] = {
// Only preamble
R"cpp(^#include "a.h")cpp",
// Both preamble and mainfile
R"cpp(
^#include "a.h"
garbage, finishes preamble
#include "a.h")cpp",
// Mixed directives
R"cpp(
^#include "a.h"
#pragma directive
// some comments
^#include_next <a.h>
#ifdef skipped
^#import "a.h"
#endif)cpp",
// Broken directives
R"cpp(
#include "a
^#include "a.h"
#include <b
^#include <b.h>)cpp",
// Directive is not part of preamble if it is not the token immediately
// followed by the hash (#).
R"cpp(
^#include "a.h"
#/**/include <b.h>)cpp",
};
for (const auto &Case : Cases) {
Annotations Test(Case);
const auto Code = Test.code();
SCOPED_TRACE(Code);
auto Includes =
collectPatchedIncludes(Code, /*BaselineContents=*/"").MainFileIncludes;
auto Points = Test.points();
ASSERT_EQ(Includes.size(), Points.size());
for (size_t I = 0, E = Includes.size(); I != E; ++I)
EXPECT_EQ(Includes[I].HashLine, Points[I].line);
}
}
TEST(PreamblePatchTest, ContainsNewIncludes) {
constexpr llvm::StringLiteral BaselineContents = R"cpp(
#include <a.h>
#include <b.h> // This will be removed
#include <c.h>
)cpp";
constexpr llvm::StringLiteral ModifiedContents = R"cpp(
#include <a.h>
#include <c.h> // This has changed a line.
#include <c.h> // This is a duplicate.
#include <d.h> // This is newly introduced.
)cpp";
auto Includes = collectPatchedIncludes(ModifiedContents, BaselineContents)
.MainFileIncludes;
EXPECT_THAT(Includes, ElementsAre(AllOf(Field(&Inclusion::Written, "<d.h>"),
Field(&Inclusion::HashLine, 4))));
}
TEST(PreamblePatchTest, MainFileIsEscaped) {
auto Includes = collectPatchedIncludes("#include <a.h>", "", "file\"name.cpp")
.MainFileIncludes;
EXPECT_THAT(Includes, ElementsAre(AllOf(Field(&Inclusion::Written, "<a.h>"),
Field(&Inclusion::HashLine, 0))));
}
TEST(PreamblePatchTest, PatchesPreambleIncludes) {
MockFS FS;
IgnoreDiagnostics Diags;
auto TU = TestTU::withCode(R"cpp(
#include "a.h"
#include "c.h"
)cpp");
TU.AdditionalFiles["a.h"] = "#include \"b.h\"";
TU.AdditionalFiles["b.h"] = "";
TU.AdditionalFiles["c.h"] = "";
auto PI = TU.inputs(FS);
auto BaselinePreamble = buildPreamble(
TU.Filename, *buildCompilerInvocation(PI, Diags), PI, true, nullptr);
// We drop c.h from modified and add a new header. Since the latter is patched
// we should only get a.h in preamble includes.
TU.Code = R"cpp(
#include "a.h"
#include "b.h"
)cpp";
auto PP = PreamblePatch::create(testPath(TU.Filename), TU.inputs(FS),
*BaselinePreamble);
// Only a.h should exists in the preamble, as c.h has been dropped and b.h was
// newly introduced.
EXPECT_THAT(PP.preambleIncludes(),
ElementsAre(AllOf(Field(&Inclusion::Written, "\"a.h\""),
Field(&Inclusion::Resolved, testPath("a.h")))));
}
llvm::Optional<ParsedAST> createPatchedAST(llvm::StringRef Baseline,
llvm::StringRef Modified) {
auto BaselinePreamble = TestTU::withCode(Baseline).preamble();
if (!BaselinePreamble) {
ADD_FAILURE() << "Failed to build baseline preamble";
return llvm::None;
}
IgnoreDiagnostics Diags;
MockFS FS;
auto TU = TestTU::withCode(Modified);
auto CI = buildCompilerInvocation(TU.inputs(FS), Diags);
if (!CI) {
ADD_FAILURE() << "Failed to build compiler invocation";
return llvm::None;
}
return ParsedAST::build(testPath(TU.Filename), TU.inputs(FS), std::move(CI),
{}, BaselinePreamble);
}
std::string getPreamblePatch(llvm::StringRef Baseline,
llvm::StringRef Modified) {
auto BaselinePreamble = TestTU::withCode(Baseline).preamble();
if (!BaselinePreamble) {
ADD_FAILURE() << "Failed to build baseline preamble";
return "";
}
MockFS FS;
auto TU = TestTU::withCode(Modified);
return PreamblePatch::create(testPath("main.cpp"), TU.inputs(FS),
*BaselinePreamble)
.text()
.str();
}
TEST(PreamblePatchTest, Define) {
// BAR should be defined while parsing the AST.
struct {
const char *const Contents;
const char *const ExpectedPatch;
} Cases[] = {
{
R"cpp(
#define BAR
[[BAR]])cpp",
R"cpp(#line 0 ".*main.cpp"
#line 2
#define BAR
)cpp",
},
// multiline macro
{
R"cpp(
#define BAR \
[[BAR]])cpp",
R"cpp(#line 0 ".*main.cpp"
#line 2
#define BAR
)cpp",
},
// multiline macro
{
R"cpp(
#define \
BAR
[[BAR]])cpp",
R"cpp(#line 0 ".*main.cpp"
#line 3
#define BAR
)cpp",
},
};
for (const auto &Case : Cases) {
SCOPED_TRACE(Case.Contents);
Annotations Modified(Case.Contents);
EXPECT_THAT(getPreamblePatch("", Modified.code()),
MatchesRegex(Case.ExpectedPatch));
auto AST = createPatchedAST("", Modified.code());
ASSERT_TRUE(AST);
std::vector<Range> MacroRefRanges;
for (auto &M : AST->getMacros().MacroRefs) {
for (auto &O : M.getSecond())
MacroRefRanges.push_back(O.Rng);
}
EXPECT_THAT(MacroRefRanges, Contains(Modified.range()));
}
}
TEST(PreamblePatchTest, OrderingPreserved) {
llvm::StringLiteral Baseline = "#define BAR(X) X";
Annotations Modified(R"cpp(
#define BAR(X, Y) X Y
#define BAR(X) X
[[BAR]](int y);
)cpp");
llvm::StringLiteral ExpectedPatch(R"cpp(#line 0 ".*main.cpp"
#line 2
#define BAR\(X, Y\) X Y
#line 3
#define BAR\(X\) X
)cpp");
EXPECT_THAT(getPreamblePatch(Baseline, Modified.code()),
MatchesRegex(ExpectedPatch.str()));
auto AST = createPatchedAST(Baseline, Modified.code());
ASSERT_TRUE(AST);
}
TEST(PreamblePatchTest, LocateMacroAtWorks) {
struct {
const char *const Baseline;
const char *const Modified;
} Cases[] = {
// Addition of new directive
{
"",
R"cpp(
#define $def^FOO
$use^FOO)cpp",
},
// Available inside preamble section
{
"",
R"cpp(
#define $def^FOO
#undef $use^FOO)cpp",
},
// Available after undef, as we don't patch those
{
"",
R"cpp(
#define $def^FOO
#undef FOO
$use^FOO)cpp",
},
// Identifier on a different line
{
"",
R"cpp(
#define \
$def^FOO
$use^FOO)cpp",
},
// In presence of comment tokens
{
"",
R"cpp(
#\
define /* FOO */\
/* FOO */ $def^FOO
$use^FOO)cpp",
},
// Moved around
{
"#define FOO",
R"cpp(
#define BAR
#define $def^FOO
$use^FOO)cpp",
},
};
for (const auto &Case : Cases) {
SCOPED_TRACE(Case.Modified);
llvm::Annotations Modified(Case.Modified);
auto AST = createPatchedAST(Case.Baseline, Modified.code());
ASSERT_TRUE(AST);
const auto &SM = AST->getSourceManager();
auto *MacroTok = AST->getTokens().spelledTokenAt(
SM.getComposedLoc(SM.getMainFileID(), Modified.point("use")));
ASSERT_TRUE(MacroTok);
auto FoundMacro = locateMacroAt(*MacroTok, AST->getPreprocessor());
ASSERT_TRUE(FoundMacro);
EXPECT_THAT(FoundMacro->Name, "FOO");
auto MacroLoc = FoundMacro->NameLoc;
EXPECT_EQ(SM.getFileID(MacroLoc), SM.getMainFileID());
EXPECT_EQ(SM.getFileOffset(MacroLoc), Modified.point("def"));
}
}
TEST(PreamblePatchTest, LocateMacroAtDeletion) {
{
// We don't patch deleted define directives, make sure we don't crash.
llvm::StringLiteral Baseline = "#define FOO";
llvm::Annotations Modified("^FOO");
auto AST = createPatchedAST(Baseline, Modified.code());
ASSERT_TRUE(AST);
const auto &SM = AST->getSourceManager();
auto *MacroTok = AST->getTokens().spelledTokenAt(
SM.getComposedLoc(SM.getMainFileID(), Modified.point()));
ASSERT_TRUE(MacroTok);
auto FoundMacro = locateMacroAt(*MacroTok, AST->getPreprocessor());
ASSERT_TRUE(FoundMacro);
EXPECT_THAT(FoundMacro->Name, "FOO");
auto HI =
getHover(*AST, offsetToPosition(Modified.code(), Modified.point()),
format::getLLVMStyle(), nullptr);
ASSERT_TRUE(HI);
EXPECT_THAT(HI->Definition, testing::IsEmpty());
}
{
// Offset is valid, but underlying text is different.
llvm::StringLiteral Baseline = "#define FOO";
Annotations Modified(R"cpp(#define BAR
^FOO")cpp");
auto AST = createPatchedAST(Baseline, Modified.code());
ASSERT_TRUE(AST);
auto HI = getHover(*AST, Modified.point(), format::getLLVMStyle(), nullptr);
ASSERT_TRUE(HI);
EXPECT_THAT(HI->Definition, "#define BAR");
}
}
MATCHER_P(referenceRangeIs, R, "") { return arg.Loc.range == R; }
TEST(PreamblePatchTest, RefsToMacros) {
struct {
const char *const Baseline;
const char *const Modified;
} Cases[] = {
// Newly added
{
"",
R"cpp(
#define ^FOO
^[[FOO]])cpp",
},
// Moved around
{
"#define FOO",
R"cpp(
#define BAR
#define ^FOO
^[[FOO]])cpp",
},
// Ref in preamble section
{
"",
R"cpp(
#define ^FOO
#undef ^FOO)cpp",
},
};
for (const auto &Case : Cases) {
Annotations Modified(Case.Modified);
auto AST = createPatchedAST("", Modified.code());
ASSERT_TRUE(AST);
const auto &SM = AST->getSourceManager();
std::vector<Matcher<ReferencesResult::Reference>> ExpectedLocations;
for (const auto &R : Modified.ranges())
ExpectedLocations.push_back(referenceRangeIs(R));
for (const auto &P : Modified.points()) {
auto *MacroTok = AST->getTokens().spelledTokenAt(SM.getComposedLoc(
SM.getMainFileID(),
llvm::cantFail(positionToOffset(Modified.code(), P))));
ASSERT_TRUE(MacroTok);
EXPECT_THAT(findReferences(*AST, P, 0).References,
testing::ElementsAreArray(ExpectedLocations));
}
}
}
TEST(TranslatePreamblePatchLocation, Simple) {
auto TU = TestTU::withHeaderCode(R"cpp(
#line 3 "main.cpp"
int foo();)cpp");
// Presumed line/col needs to be valid in the main file.
TU.Code = R"cpp(// line 1
// line 2
// line 3
// line 4)cpp";
TU.Filename = "main.cpp";
TU.HeaderFilename = "__preamble_patch__.h";
TU.ImplicitHeaderGuard = false;
auto AST = TU.build();
auto &SM = AST.getSourceManager();
auto &ND = findDecl(AST, "foo");
EXPECT_NE(SM.getFileID(ND.getLocation()), SM.getMainFileID());
auto TranslatedLoc = translatePreamblePatchLocation(ND.getLocation(), SM);
auto DecompLoc = SM.getDecomposedLoc(TranslatedLoc);
EXPECT_EQ(DecompLoc.first, SM.getMainFileID());
EXPECT_EQ(SM.getLineNumber(DecompLoc.first, DecompLoc.second), 3U);
}
TEST(PreamblePatch, ModifiedBounds) {
struct {
const char *const Baseline;
const char *const Modified;
} Cases[] = {
// Size increased
{
"",
R"cpp(
#define FOO
FOO)cpp",
},
// Stayed same
{"#define FOO", "#define BAR"},
// Got smaller
{
R"cpp(
#define FOO
#undef FOO)cpp",
"#define FOO"},
};
for (const auto &Case : Cases) {
auto TU = TestTU::withCode(Case.Baseline);
auto BaselinePreamble = TU.preamble();
ASSERT_TRUE(BaselinePreamble);
Annotations Modified(Case.Modified);
TU.Code = Modified.code().str();
MockFS FS;
auto PP = PreamblePatch::create(testPath(TU.Filename), TU.inputs(FS),
*BaselinePreamble);
IgnoreDiagnostics Diags;
auto CI = buildCompilerInvocation(TU.inputs(FS), Diags);
ASSERT_TRUE(CI);
const auto ExpectedBounds =
Lexer::ComputePreamble(Case.Modified, *CI->getLangOpts());
EXPECT_EQ(PP.modifiedBounds().Size, ExpectedBounds.Size);
EXPECT_EQ(PP.modifiedBounds().PreambleEndsAtStartOfLine,
ExpectedBounds.PreambleEndsAtStartOfLine);
}
}
TEST(PreamblePatch, DropsDiagnostics) {
llvm::StringLiteral Code = "#define FOO\nx;/* error-ok */";
// First check that this code generates diagnostics.
EXPECT_THAT(*TestTU::withCode(Code).build().getDiagnostics(),
testing::Not(testing::IsEmpty()));
// Ensure they are dropeed when a patched preamble is used.
EXPECT_FALSE(createPatchedAST("", Code)->getDiagnostics());
}
} // namespace
} // namespace clangd
} // namespace clang