blob: ab367fca66e11acde11092c028f7f9c64b9b48d4 [file] [log] [blame]
//===--- IncludeCleanerTests.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 "IncludeCleaner.h"
#include "SourceCode.h"
#include "TestTU.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/Testing/Support/SupportHelpers.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace clang {
namespace clangd {
namespace {
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
using ::testing::IsEmpty;
using ::testing::Pointee;
using ::testing::UnorderedElementsAre;
std::string guard(llvm::StringRef Code) {
return "#pragma once\n" + Code.str();
}
TEST(IncludeCleaner, ReferencedLocations) {
struct TestCase {
std::string HeaderCode;
std::string MainCode;
};
TestCase Cases[] = {
// DeclRefExpr
{
"int ^x();",
"int y = x();",
},
// RecordDecl
{
"class ^X;",
"X *y;",
},
// When definition is available, we don't need to mark forward
// declarations as used.
{
"class ^X {}; class X;",
"X *y;",
},
// We already have forward declaration in the main file, the other
// non-definition declarations are not needed.
{
"class ^X {}; class X;",
"class X; X *y;",
},
// Nested class definition can occur outside of the parent class
// definition. Bar declaration should be visible to its definition but
// it will always be because we will mark Foo definition as used.
{
"class ^Foo { class Bar; };",
"class Foo::Bar {};",
},
// TypedefType and UsingDecls
{
"using ^Integer = int;",
"Integer x;",
},
{
"namespace ns { void ^foo(); void ^foo() {} }",
"using ns::foo;",
},
{
"namespace ns { void foo(); void foo() {}; }",
"using namespace ns;",
},
{
"struct ^A {}; using B = A; using ^C = B;",
"C a;",
},
{
"typedef bool ^Y; template <typename T> struct ^X {};",
"X<Y> x;",
},
{
"struct Foo; struct ^Foo{}; typedef Foo ^Bar;",
"Bar b;",
},
{
"namespace ns { class X; }; using ns::^X;",
"X *y;",
},
// MemberExpr
{
"struct ^X{int ^a;}; X ^foo();",
"int y = foo().a;",
},
// Expr (type is traversed)
{
"class ^X{}; X ^foo();",
"auto bar() { return foo(); }",
},
// Redecls
{
"void ^foo(); void ^foo() {} void ^foo();",
"void bar() { foo(); }",
},
// Constructor
{
"struct ^X { ^X(int) {} int ^foo(); };",
"auto x = X(42); auto y = x.foo();",
},
// Function
{
"void ^foo();",
"void foo() {}",
},
{
"void foo() {}",
"void foo();",
},
{
"inline void ^foo() {}",
"void bar() { foo(); }",
},
{
"int ^foo(char); int ^foo(float);",
"template<class T> int x = foo(T{});",
},
// Static function
{
"struct ^X { static bool ^foo(); }; bool X::^foo() {}",
"auto b = X::foo();",
},
// TemplateRecordDecl
{
"template <typename> class ^X;",
"X<int> *y;",
},
// Type name not spelled out in code
{
"class ^X{}; X ^getX();",
"auto x = getX();",
},
// Enums
{
"enum ^Color { ^Red = 42, Green = 9000};",
"int MyColor = Red;",
},
{
"struct ^X { enum ^Language { ^CXX = 42, Python = 9000}; };",
"int Lang = X::CXX;",
},
// Macros
{
"#define ^CONSTANT 42",
"int Foo = CONSTANT;",
},
{
"#define ^FOO x",
"#define BAR FOO",
},
{
"#define INNER 42\n"
"#define ^OUTER INNER",
"int answer = OUTER;",
},
{
"#define ^ANSWER 42\n"
"#define ^SQUARE(X) X * X",
"int sq = SQUARE(ANSWER);",
},
{
"#define ^FOO\n"
"#define ^BAR",
"#if 0\n"
"#if FOO\n"
"BAR\n"
"#endif\n"
"#endif",
},
// Misc
{
"enum class ^Color : int;",
"enum class Color : int {};",
},
{
"enum class Color : int {};",
"enum class Color : int;",
},
{
"enum class ^Color;",
"Color c;",
},
{
"enum class ^Color : int;",
"Color c;",
},
{
"enum class ^Color : char;",
"Color *c;",
},
{
"enum class ^Color : char {};",
"Color *c;",
}};
for (const TestCase &T : Cases) {
TestTU TU;
TU.Code = T.MainCode;
Annotations Header(T.HeaderCode);
TU.HeaderCode = Header.code().str();
auto AST = TU.build();
std::vector<Position> Points;
for (const auto &Loc : findReferencedLocations(AST).User) {
if (AST.getSourceManager().getBufferName(Loc).endswith(
TU.HeaderFilename)) {
Points.push_back(offsetToPosition(
TU.HeaderCode, AST.getSourceManager().getFileOffset(Loc)));
}
}
llvm::sort(Points);
EXPECT_EQ(Points, Header.points()) << T.HeaderCode << "\n---\n"
<< T.MainCode;
}
}
TEST(IncludeCleaner, Stdlib) {
// Smoke tests only for finding used symbols/headers.
// Details of Decl -> stdlib::Symbol -> stdlib::Headers mapping tested there.
auto TU = TestTU::withHeaderCode(R"cpp(
namespace std { class error_code {}; }
class error_code {};
namespace nonstd { class error_code {}; }
)cpp");
struct {
llvm::StringRef Code;
std::vector<llvm::StringRef> Symbols;
std::vector<llvm::StringRef> Headers;
} Tests[] = {
{"std::error_code x;", {"std::error_code"}, {"<system_error>"}},
{"error_code x;", {}, {}},
{"nonstd::error_code x;", {}, {}},
};
for (const auto &Test : Tests) {
TU.Code = Test.Code.str();
ParsedAST AST = TU.build();
std::vector<stdlib::Symbol> WantSyms;
for (const auto &SymName : Test.Symbols) {
auto QName = splitQualifiedName(SymName);
auto Sym = stdlib::Symbol::named(QName.first, QName.second);
EXPECT_TRUE(Sym) << SymName;
WantSyms.push_back(*Sym);
}
std::vector<stdlib::Header> WantHeaders;
for (const auto &HeaderName : Test.Headers) {
auto Header = stdlib::Header::named(HeaderName);
EXPECT_TRUE(Header) << HeaderName;
WantHeaders.push_back(*Header);
}
ReferencedLocations Locs = findReferencedLocations(AST);
EXPECT_THAT(Locs.Stdlib, ElementsAreArray(WantSyms));
ReferencedFiles Files = findReferencedFiles(Locs, AST.getIncludeStructure(),
AST.getSourceManager());
EXPECT_THAT(Files.Stdlib, ElementsAreArray(WantHeaders));
}
}
MATCHER_P(writtenInclusion, Written, "") {
if (arg.Written != Written)
*result_listener << arg.Written;
return arg.Written == Written;
}
TEST(IncludeCleaner, StdlibUnused) {
setIncludeCleanerAnalyzesStdlib(true);
auto Cleanup =
llvm::make_scope_exit([] { setIncludeCleanerAnalyzesStdlib(false); });
auto TU = TestTU::withCode(R"cpp(
#include <list>
#include <queue>
std::list<int> x;
)cpp");
// Layout of std library impl is not relevant.
TU.AdditionalFiles["bits"] = R"cpp(
#pragma once
namespace std {
template <typename> class list {};
template <typename> class queue {};
}
)cpp";
TU.AdditionalFiles["list"] = "#include <bits>";
TU.AdditionalFiles["queue"] = "#include <bits>";
TU.ExtraArgs = {"-isystem", testRoot()};
auto AST = TU.build();
auto Unused = computeUnusedIncludes(AST);
EXPECT_THAT(Unused, ElementsAre(Pointee(writtenInclusion("<queue>"))));
}
TEST(IncludeCleaner, GetUnusedHeaders) {
llvm::StringLiteral MainFile = R"cpp(
#include "a.h"
#include "b.h"
#include "dir/c.h"
#include "dir/unused.h"
#include "unguarded.h"
#include "unused.h"
#include <system_header.h>
void foo() {
a();
b();
c();
})cpp";
// Build expected ast with symbols coming from headers.
TestTU TU;
TU.Filename = "foo.cpp";
TU.AdditionalFiles["foo.h"] = guard("void foo();");
TU.AdditionalFiles["a.h"] = guard("void a();");
TU.AdditionalFiles["b.h"] = guard("void b();");
TU.AdditionalFiles["dir/c.h"] = guard("void c();");
TU.AdditionalFiles["unused.h"] = guard("void unused();");
TU.AdditionalFiles["dir/unused.h"] = guard("void dirUnused();");
TU.AdditionalFiles["system/system_header.h"] = guard("");
TU.AdditionalFiles["unguarded.h"] = "";
TU.ExtraArgs.push_back("-I" + testPath("dir"));
TU.ExtraArgs.push_back("-isystem" + testPath("system"));
TU.Code = MainFile.str();
ParsedAST AST = TU.build();
std::vector<std::string> UnusedIncludes;
for (const auto &Include : computeUnusedIncludes(AST))
UnusedIncludes.push_back(Include->Written);
EXPECT_THAT(UnusedIncludes,
UnorderedElementsAre("\"unused.h\"", "\"dir/unused.h\""));
}
TEST(IncludeCleaner, VirtualBuffers) {
TestTU TU;
TU.Code = R"cpp(
#include "macros.h"
using flags::FLAGS_FOO;
// CLI will come from a define, __llvm__ is a built-in. In both cases, they
// come from non-existent files.
int y = CLI + __llvm__;
int concat(a, b) = 42;
)cpp";
// The pasting operator in combination with DEFINE_FLAG will create
// ScratchBuffer with `flags::FLAGS_FOO` that will have FileID but not
// FileEntry.
TU.AdditionalFiles["macros.h"] = R"cpp(
#ifndef MACROS_H
#define MACROS_H
#define DEFINE_FLAG(X) \
namespace flags { \
int FLAGS_##X; \
} \
DEFINE_FLAG(FOO)
#define ab x
#define concat(x, y) x##y
#endif // MACROS_H
)cpp";
TU.ExtraArgs = {"-DCLI=42"};
ParsedAST AST = TU.build();
auto &SM = AST.getSourceManager();
auto &Includes = AST.getIncludeStructure();
auto ReferencedFiles =
findReferencedFiles(findReferencedLocations(AST), Includes, SM);
llvm::StringSet<> ReferencedFileNames;
for (FileID FID : ReferencedFiles.User)
ReferencedFileNames.insert(
SM.getPresumedLoc(SM.getLocForStartOfFile(FID)).getFilename());
// Note we deduped the names as _number_ of <built-in>s is uninteresting.
EXPECT_THAT(ReferencedFileNames.keys(),
UnorderedElementsAre("<built-in>", "<scratch space>",
testPath("macros.h")));
// Should not crash due to FileIDs that are not headers.
auto ReferencedHeaders = translateToHeaderIDs(ReferencedFiles, Includes, SM);
std::vector<llvm::StringRef> ReferencedHeaderNames;
for (IncludeStructure::HeaderID HID : ReferencedHeaders)
ReferencedHeaderNames.push_back(Includes.getRealPath(HID));
// Non-header files are gone at this point.
EXPECT_THAT(ReferencedHeaderNames, ElementsAre(testPath("macros.h")));
// Sanity check.
EXPECT_THAT(getUnused(AST, ReferencedHeaders), IsEmpty());
}
TEST(IncludeCleaner, DistinctUnguardedInclusions) {
TestTU TU;
TU.Code = R"cpp(
#include "bar.h"
#include "foo.h"
int LocalFoo = foo::Variable;
)cpp";
TU.AdditionalFiles["foo.h"] = R"cpp(
#pragma once
namespace foo {
#include "unguarded.h"
}
)cpp";
TU.AdditionalFiles["bar.h"] = R"cpp(
#pragma once
namespace bar {
#include "unguarded.h"
}
)cpp";
TU.AdditionalFiles["unguarded.h"] = R"cpp(
constexpr int Variable = 42;
)cpp";
ParsedAST AST = TU.build();
auto ReferencedFiles =
findReferencedFiles(findReferencedLocations(AST),
AST.getIncludeStructure(), AST.getSourceManager());
llvm::StringSet<> ReferencedFileNames;
auto &SM = AST.getSourceManager();
for (FileID FID : ReferencedFiles.User)
ReferencedFileNames.insert(
SM.getPresumedLoc(SM.getLocForStartOfFile(FID)).getFilename());
// Note that we have uplifted the referenced files from non self-contained
// headers to header-guarded ones.
EXPECT_THAT(ReferencedFileNames.keys(),
UnorderedElementsAre(testPath("foo.h")));
}
TEST(IncludeCleaner, NonSelfContainedHeaders) {
TestTU TU;
TU.Code = R"cpp(
#include "foo.h"
int LocalFoo = Variable;
)cpp";
TU.AdditionalFiles["foo.h"] = R"cpp(
#pragma once
#include "indirection.h"
)cpp";
TU.AdditionalFiles["indirection.h"] = R"cpp(
#include "unguarded.h"
)cpp";
TU.AdditionalFiles["unguarded.h"] = R"cpp(
constexpr int Variable = 42;
)cpp";
ParsedAST AST = TU.build();
auto ReferencedFiles =
findReferencedFiles(findReferencedLocations(AST),
AST.getIncludeStructure(), AST.getSourceManager());
llvm::StringSet<> ReferencedFileNames;
auto &SM = AST.getSourceManager();
for (FileID FID : ReferencedFiles.User)
ReferencedFileNames.insert(
SM.getPresumedLoc(SM.getLocForStartOfFile(FID)).getFilename());
// Note that we have uplifted the referenced files from non self-contained
// headers to header-guarded ones.
EXPECT_THAT(ReferencedFileNames.keys(),
UnorderedElementsAre(testPath("foo.h")));
}
TEST(IncludeCleaner, IWYUPragmas) {
TestTU TU;
TU.Code = R"cpp(
#include "behind_keep.h" // IWYU pragma: keep
)cpp";
TU.AdditionalFiles["behind_keep.h"] = guard("");
ParsedAST AST = TU.build();
auto ReferencedFiles =
findReferencedFiles(findReferencedLocations(AST),
AST.getIncludeStructure(), AST.getSourceManager());
EXPECT_TRUE(ReferencedFiles.User.empty());
EXPECT_THAT(AST.getDiagnostics(), llvm::ValueIs(IsEmpty()));
}
} // namespace
} // namespace clangd
} // namespace clang