blob: 89a3cc060f925c266283cf4ea41571fad8a8349e [file] [log] [blame]
//===--- DiagnosticsTests.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 "Config.h"
#include "Diagnostics.h"
#include "Feature.h"
#include "FeatureModule.h"
#include "ParsedAST.h"
#include "Protocol.h"
#include "SourceCode.h"
#include "TestFS.h"
#include "TestIndex.h"
#include "TestTU.h"
#include "TidyProvider.h"
#include "index/MemIndex.h"
#include "support/Context.h"
#include "support/Path.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/DiagnosticSema.h"
#include "llvm/Support/ScopedPrinter.h"
#include "llvm/Support/TargetSelect.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <algorithm>
#include <memory>
namespace clang {
namespace clangd {
namespace {
using ::testing::_;
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Pair;
using ::testing::SizeIs;
using ::testing::UnorderedElementsAre;
using testing::UnorderedElementsAreArray;
::testing::Matcher<const Diag &> WithFix(::testing::Matcher<Fix> FixMatcher) {
return Field(&Diag::Fixes, ElementsAre(FixMatcher));
}
::testing::Matcher<const Diag &> WithFix(::testing::Matcher<Fix> FixMatcher1,
::testing::Matcher<Fix> FixMatcher2) {
return Field(&Diag::Fixes, UnorderedElementsAre(FixMatcher1, FixMatcher2));
}
::testing::Matcher<const Diag &>
WithNote(::testing::Matcher<Note> NoteMatcher) {
return Field(&Diag::Notes, ElementsAre(NoteMatcher));
}
::testing::Matcher<const Diag &>
WithNote(::testing::Matcher<Note> NoteMatcher1,
::testing::Matcher<Note> NoteMatcher2) {
return Field(&Diag::Notes, UnorderedElementsAre(NoteMatcher1, NoteMatcher2));
}
::testing::Matcher<const Diag &>
WithTag(::testing::Matcher<DiagnosticTag> TagMatcher) {
return Field(&Diag::Tags, Contains(TagMatcher));
}
MATCHER_P2(Diag, Range, Message,
"Diag at " + llvm::to_string(Range) + " = [" + Message + "]") {
return arg.Range == Range && arg.Message == Message;
}
MATCHER_P3(Fix, Range, Replacement, Message,
"Fix " + llvm::to_string(Range) + " => " +
::testing::PrintToString(Replacement) + " = [" + Message + "]") {
return arg.Message == Message && arg.Edits.size() == 1 &&
arg.Edits[0].range == Range && arg.Edits[0].newText == Replacement;
}
MATCHER_P(FixMessage, Message, "") { return arg.Message == Message; }
MATCHER_P(EqualToLSPDiag, LSPDiag,
"LSP diagnostic " + llvm::to_string(LSPDiag)) {
if (toJSON(arg) != toJSON(LSPDiag)) {
*result_listener << llvm::formatv("expected:\n{0:2}\ngot\n{1:2}",
toJSON(LSPDiag), toJSON(arg))
.str();
return false;
}
return true;
}
MATCHER_P(DiagSource, S, "") { return arg.Source == S; }
MATCHER_P(DiagName, N, "") { return arg.Name == N; }
MATCHER_P(DiagSeverity, S, "") { return arg.Severity == S; }
MATCHER_P(EqualToFix, Fix, "LSP fix " + llvm::to_string(Fix)) {
if (arg.Message != Fix.Message)
return false;
if (arg.Edits.size() != Fix.Edits.size())
return false;
for (std::size_t I = 0; I < arg.Edits.size(); ++I) {
if (arg.Edits[I].range != Fix.Edits[I].range ||
arg.Edits[I].newText != Fix.Edits[I].newText)
return false;
}
return true;
}
// Helper function to make tests shorter.
Position pos(int line, int character) {
Position Res;
Res.line = line;
Res.character = character;
return Res;
}
// Normally returns the provided diagnostics matcher.
// If clang-tidy checks are not linked in, returns a matcher for no diagnostics!
// This is intended for tests where the diagnostics come from clang-tidy checks.
// We don't #ifdef each individual test as it's intrusive and we want to ensure
// that as much of the test is still compiled an run as possible.
::testing::Matcher<std::vector<clangd::Diag>>
ifTidyChecks(::testing::Matcher<std::vector<clangd::Diag>> M) {
if (!CLANGD_TIDY_CHECKS)
return IsEmpty();
return M;
}
TEST(DiagnosticsTest, DiagnosticRanges) {
// Check we report correct ranges, including various edge-cases.
Annotations Test(R"cpp(
// error-ok
#define ID(X) X
namespace test{};
void $decl[[foo]]();
int main() {
struct Container { int* begin(); int* end(); } *container;
for (auto i : $insertstar[[]]$range[[container]]) {
}
$typo[[go\
o]]();
foo()$semicolon[[]]//with comments
$unk[[unknown]]();
double $type[[bar]] = "foo";
struct Foo { int x; }; Foo a;
a.$nomember[[y]];
test::$nomembernamespace[[test]];
$macro[[ID($macroarg[[fod]])]]();
}
)cpp");
auto TU = TestTU::withCode(Test.code());
EXPECT_THAT(
*TU.build().getDiagnostics(),
ElementsAre(
// Make sure the whole token is highlighted.
AllOf(Diag(Test.range("range"),
"invalid range expression of type 'struct Container *'; "
"did you mean to dereference it with '*'?"),
WithFix(Fix(Test.range("insertstar"), "*", "insert '*'"))),
// This range spans lines.
AllOf(Diag(Test.range("typo"),
"use of undeclared identifier 'goo'; did you mean 'foo'?"),
DiagSource(Diag::Clang), DiagName("undeclared_var_use_suggest"),
WithFix(
Fix(Test.range("typo"), "foo", "change 'go\\…' to 'foo'")),
// This is a pretty normal range.
WithNote(Diag(Test.range("decl"), "'foo' declared here"))),
// This range is zero-width and insertion. Therefore make sure we are
// not expanding it into other tokens. Since we are not going to
// replace those.
AllOf(Diag(Test.range("semicolon"), "expected ';' after expression"),
WithFix(Fix(Test.range("semicolon"), ";", "insert ';'"))),
// This range isn't provided by clang, we expand to the token.
Diag(Test.range("unk"), "use of undeclared identifier 'unknown'"),
Diag(Test.range("type"),
"cannot initialize a variable of type 'double' with an lvalue "
"of type 'const char[4]'"),
Diag(Test.range("nomember"), "no member named 'y' in 'Foo'"),
Diag(Test.range("nomembernamespace"),
"no member named 'test' in namespace 'test'"),
AllOf(Diag(Test.range("macro"),
"use of undeclared identifier 'fod'; did you mean 'foo'?"),
WithFix(Fix(Test.range("macroarg"), "foo",
"change 'fod' to 'foo'")))));
}
// Verify that the -Wswitch case-not-covered diagnostic range covers the
// whole expression. This is important because the "populate-switch" tweak
// fires for the full expression range (see tweaks/PopulateSwitchTests.cpp).
// The quickfix flow only works end-to-end if the tweak can be triggered on
// the diagnostic's range.
TEST(DiagnosticsTest, WSwitch) {
Annotations Test(R"cpp(
enum A { X };
struct B { A a; };
void foo(B b) {
switch ([[b.a]]) {}
}
)cpp");
auto TU = TestTU::withCode(Test.code());
TU.ExtraArgs = {"-Wswitch"};
EXPECT_THAT(*TU.build().getDiagnostics(),
ElementsAre(Diag(Test.range(),
"enumeration value 'X' not handled in switch")));
}
TEST(DiagnosticsTest, FlagsMatter) {
Annotations Test("[[void]] main() {} // error-ok");
auto TU = TestTU::withCode(Test.code());
EXPECT_THAT(*TU.build().getDiagnostics(),
ElementsAre(AllOf(Diag(Test.range(), "'main' must return 'int'"),
WithFix(Fix(Test.range(), "int",
"change 'void' to 'int'")))));
// Same code built as C gets different diagnostics.
TU.Filename = "Plain.c";
EXPECT_THAT(
*TU.build().getDiagnostics(),
ElementsAre(AllOf(
Diag(Test.range(), "return type of 'main' is not 'int'"),
WithFix(Fix(Test.range(), "int", "change return type to 'int'")))));
}
TEST(DiagnosticsTest, DiagnosticPreamble) {
Annotations Test(R"cpp(
#include $[["not-found.h"]] // error-ok
)cpp");
auto TU = TestTU::withCode(Test.code());
EXPECT_THAT(*TU.build().getDiagnostics(),
ElementsAre(::testing::AllOf(
Diag(Test.range(), "'not-found.h' file not found"),
DiagSource(Diag::Clang), DiagName("pp_file_not_found"))));
}
TEST(DiagnosticsTest, DeduplicatedClangTidyDiagnostics) {
Annotations Test(R"cpp(
float foo = [[0.1f]];
)cpp");
auto TU = TestTU::withCode(Test.code());
// Enable alias clang-tidy checks, these check emits the same diagnostics
// (except the check name).
TU.ClangTidyProvider = addTidyChecks("readability-uppercase-literal-suffix,"
"hicpp-uppercase-literal-suffix");
// Verify that we filter out the duplicated diagnostic message.
EXPECT_THAT(
*TU.build().getDiagnostics(),
ifTidyChecks(UnorderedElementsAre(::testing::AllOf(
Diag(Test.range(),
"floating point literal has suffix 'f', which is not uppercase"),
DiagSource(Diag::ClangTidy)))));
Test = Annotations(R"cpp(
template<typename T>
void func(T) {
float f = [[0.3f]];
}
void k() {
func(123);
func(2.0);
}
)cpp");
TU.Code = std::string(Test.code());
// The check doesn't handle template instantiations which ends up emitting
// duplicated messages, verify that we deduplicate them.
EXPECT_THAT(
*TU.build().getDiagnostics(),
ifTidyChecks(UnorderedElementsAre(::testing::AllOf(
Diag(Test.range(),
"floating point literal has suffix 'f', which is not uppercase"),
DiagSource(Diag::ClangTidy)))));
}
TEST(DiagnosticsTest, ClangTidy) {
Annotations Test(R"cpp(
#include $deprecated[["assert.h"]]
#define $macrodef[[SQUARE]](X) (X)*(X)
int $main[[main]]() {
int y = 4;
return SQUARE($macroarg[[++]]y);
return $doubled[[sizeof]](sizeof(int));
}
// misc-no-recursion uses a custom traversal from the TUDecl
void foo();
void $bar[[bar]]() {
foo();
}
void $foo[[foo]]() {
bar();
}
)cpp");
auto TU = TestTU::withCode(Test.code());
TU.HeaderFilename = "assert.h"; // Suppress "not found" error.
TU.ClangTidyProvider = addTidyChecks("bugprone-sizeof-expression,"
"bugprone-macro-repeated-side-effects,"
"modernize-deprecated-headers,"
"modernize-use-trailing-return-type,"
"misc-no-recursion");
TU.ExtraArgs.push_back("-Wno-unsequenced");
EXPECT_THAT(
*TU.build().getDiagnostics(),
ifTidyChecks(UnorderedElementsAre(
AllOf(Diag(Test.range("deprecated"),
"inclusion of deprecated C++ header 'assert.h'; consider "
"using 'cassert' instead"),
DiagSource(Diag::ClangTidy),
DiagName("modernize-deprecated-headers"),
WithFix(Fix(Test.range("deprecated"), "<cassert>",
"change '\"assert.h\"' to '<cassert>'"))),
Diag(Test.range("doubled"),
"suspicious usage of 'sizeof(sizeof(...))'"),
AllOf(Diag(Test.range("macroarg"),
"side effects in the 1st macro argument 'X' are "
"repeated in "
"macro expansion"),
DiagSource(Diag::ClangTidy),
DiagName("bugprone-macro-repeated-side-effects"),
WithNote(Diag(Test.range("macrodef"),
"macro 'SQUARE' defined here"))),
AllOf(Diag(Test.range("main"),
"use a trailing return type for this function"),
DiagSource(Diag::ClangTidy),
DiagName("modernize-use-trailing-return-type"),
// Verify there's no "[check-name]" suffix in the message.
WithFix(FixMessage(
"use a trailing return type for this function"))),
Diag(Test.range("foo"),
"function 'foo' is within a recursive call chain"),
Diag(Test.range("bar"),
"function 'bar' is within a recursive call chain"))));
}
TEST(DiagnosticsTest, ClangTidyEOF) {
// clang-format off
Annotations Test(R"cpp(
[[#]]include <b.h>
#include "a.h")cpp");
// clang-format on
auto TU = TestTU::withCode(Test.code());
TU.ExtraArgs = {"-isystem."};
TU.AdditionalFiles["a.h"] = TU.AdditionalFiles["b.h"] = "";
TU.ClangTidyProvider = addTidyChecks("llvm-include-order");
EXPECT_THAT(
*TU.build().getDiagnostics(),
ifTidyChecks(Contains(
AllOf(Diag(Test.range(), "#includes are not sorted properly"),
DiagSource(Diag::ClangTidy), DiagName("llvm-include-order")))));
}
TEST(DiagnosticTest, TemplatesInHeaders) {
// Diagnostics from templates defined in headers are placed at the expansion.
Annotations Main(R"cpp(
Derived<int> [[y]]; // error-ok
)cpp");
Annotations Header(R"cpp(
template <typename T>
struct Derived : [[T]] {};
)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.HeaderCode = Header.code().str();
EXPECT_THAT(
*TU.build().getDiagnostics(),
ElementsAre(AllOf(
Diag(Main.range(), "in template: base specifier must name a class"),
WithNote(Diag(Header.range(), "error occurred here"),
Diag(Main.range(), "in instantiation of template class "
"'Derived<int>' requested here")))));
}
TEST(DiagnosticTest, MakeUnique) {
// We usually miss diagnostics from header functions as we don't parse them.
// std::make_unique is an exception.
Annotations Main(R"cpp(
struct S { S(char*); };
auto x = std::[[make_unique]]<S>(42); // error-ok
)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.HeaderCode = R"cpp(
namespace std {
// These mocks aren't quite right - we omit unique_ptr for simplicity.
// forward is included to show its body is not needed to get the diagnostic.
template <typename T> T&& forward(T& t) { return static_cast<T&&>(t); }
template <typename T, typename... A> T* make_unique(A&&... args) {
return new T(std::forward<A>(args)...);
}
}
)cpp";
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(
Diag(Main.range(),
"in template: "
"no matching constructor for initialization of 'S'")));
}
TEST(DiagnosticTest, NoMultipleDiagnosticInFlight) {
Annotations Main(R"cpp(
template <typename T> struct Foo {
T *begin();
T *end();
};
struct LabelInfo {
int a;
bool b;
};
void f() {
Foo<LabelInfo> label_info_map;
[[for]] (auto it = label_info_map.begin(); it != label_info_map.end(); ++it) {
auto S = *it;
}
}
)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.ClangTidyProvider = addTidyChecks("modernize-loop-convert");
EXPECT_THAT(
*TU.build().getDiagnostics(),
ifTidyChecks(UnorderedElementsAre(::testing::AllOf(
Diag(Main.range(), "use range-based for loop instead"),
DiagSource(Diag::ClangTidy), DiagName("modernize-loop-convert")))));
}
TEST(DiagnosticTest, RespectsDiagnosticConfig) {
Annotations Main(R"cpp(
// error-ok
void x() {
[[unknown]]();
$ret[[return]] 42;
}
)cpp");
auto TU = TestTU::withCode(Main.code());
EXPECT_THAT(
*TU.build().getDiagnostics(),
ElementsAre(Diag(Main.range(), "use of undeclared identifier 'unknown'"),
Diag(Main.range("ret"),
"void function 'x' should not return a value")));
Config Cfg;
Cfg.Diagnostics.Suppress.insert("return-type");
WithContextValue WithCfg(Config::Key, std::move(Cfg));
EXPECT_THAT(*TU.build().getDiagnostics(),
ElementsAre(Diag(Main.range(),
"use of undeclared identifier 'unknown'")));
}
TEST(DiagnosticTest, RespectsDiagnosticConfigInHeader) {
Annotations Header(R"cpp(
int x = "42"; // error-ok
)cpp");
Annotations Main(R"cpp(
#include "header.hpp"
)cpp");
auto TU = TestTU::withCode(Main.code());
TU.AdditionalFiles["header.hpp"] = std::string(Header.code());
Config Cfg;
Cfg.Diagnostics.Suppress.insert("init_conversion_failed");
WithContextValue WithCfg(Config::Key, std::move(Cfg));
EXPECT_THAT(*TU.build().getDiagnostics(), IsEmpty());
}
TEST(DiagnosticTest, ClangTidySuppressionComment) {
Annotations Main(R"cpp(
int main() {
int i = 3;
double d = 8 / i; // NOLINT
// NOLINTNEXTLINE
double e = 8 / i;
#define BAD 8 / i
double f = BAD; // NOLINT
double g = [[8]] / i;
#define BAD2 BAD
double h = BAD2; // NOLINT
// NOLINTBEGIN
double x = BAD2;
double y = BAD2;
// NOLINTEND
// verify no crashes on unmatched nolints.
// NOLINTBEIGN
}
)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.ClangTidyProvider = addTidyChecks("bugprone-integer-division");
EXPECT_THAT(
*TU.build().getDiagnostics(),
ifTidyChecks(UnorderedElementsAre(::testing::AllOf(
Diag(Main.range(), "result of integer division used in a floating "
"point context; possible loss of precision"),
DiagSource(Diag::ClangTidy),
DiagName("bugprone-integer-division")))));
}
TEST(DiagnosticTest, ClangTidyWarningAsError) {
Annotations Main(R"cpp(
int main() {
int i = 3;
double f = [[8]] / i; // error-ok
}
)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.ClangTidyProvider =
addTidyChecks("bugprone-integer-division", "bugprone-integer-division");
EXPECT_THAT(
*TU.build().getDiagnostics(),
ifTidyChecks(UnorderedElementsAre(::testing::AllOf(
Diag(Main.range(), "result of integer division used in a floating "
"point context; possible loss of precision"),
DiagSource(Diag::ClangTidy), DiagName("bugprone-integer-division"),
DiagSeverity(DiagnosticsEngine::Error)))));
}
TEST(DiagnosticTest, LongFixMessages) {
// We limit the size of printed code.
Annotations Source(R"cpp(
int main() {
// error-ok
int somereallyreallyreallyreallyreallyreallyreallyreallylongidentifier;
[[omereallyreallyreallyreallyreallyreallyreallyreallylongidentifier]]= 10;
}
)cpp");
TestTU TU = TestTU::withCode(Source.code());
EXPECT_THAT(
*TU.build().getDiagnostics(),
ElementsAre(WithFix(Fix(
Source.range(),
"somereallyreallyreallyreallyreallyreallyreallyreallylongidentifier",
"change 'omereallyreallyreallyreallyreallyreallyreallyreall…' to "
"'somereallyreallyreallyreallyreallyreallyreallyreal…'"))));
// Only show changes up to a first newline.
Source = Annotations(R"cpp(
// error-ok
int main() {
int ident;
[[ide\
n]] = 10; // error-ok
}
)cpp");
TU.Code = std::string(Source.code());
EXPECT_THAT(*TU.build().getDiagnostics(),
ElementsAre(WithFix(
Fix(Source.range(), "ident", "change 'ide\\…' to 'ident'"))));
}
TEST(DiagnosticTest, ClangTidySuppressionCommentTrumpsWarningAsError) {
Annotations Main(R"cpp(
int main() {
int i = 3;
double f = [[8]] / i; // NOLINT
}
)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.ClangTidyProvider =
addTidyChecks("bugprone-integer-division", "bugprone-integer-division");
EXPECT_THAT(*TU.build().getDiagnostics(), UnorderedElementsAre());
}
TEST(DiagnosticTest, ClangTidyNoLiteralDataInMacroToken) {
Annotations Main(R"cpp(
#define SIGTERM 15
using pthread_t = int;
int pthread_kill(pthread_t thread, int sig);
int func() {
pthread_t thread;
return pthread_kill(thread, 0);
}
)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.ClangTidyProvider = addTidyChecks("bugprone-bad-signal-to-kill-thread");
EXPECT_THAT(*TU.build().getDiagnostics(), UnorderedElementsAre()); // no-crash
}
TEST(DiagnosticTest, ElseAfterReturnRange) {
Annotations Main(R"cpp(
int foo(int cond) {
if (cond == 1) {
return 42;
} [[else]] if (cond == 2) {
return 43;
}
return 44;
}
)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.ClangTidyProvider = addTidyChecks("llvm-else-after-return");
EXPECT_THAT(*TU.build().getDiagnostics(),
ifTidyChecks(ElementsAre(
Diag(Main.range(), "do not use 'else' after 'return'"))));
}
TEST(DiagnosticsTest, Preprocessor) {
// This looks like a preamble, but there's an #else in the middle!
// Check that:
// - the #else doesn't generate diagnostics (we had this bug)
// - we get diagnostics from the taken branch
// - we get no diagnostics from the not taken branch
Annotations Test(R"cpp(
#ifndef FOO
#define FOO
int a = [[b]]; // error-ok
#else
int x = y;
#endif
)cpp");
EXPECT_THAT(
*TestTU::withCode(Test.code()).build().getDiagnostics(),
ElementsAre(Diag(Test.range(), "use of undeclared identifier 'b'")));
}
TEST(DiagnosticsTest, IgnoreVerify) {
auto TU = TestTU::withCode(R"cpp(
int a; // expected-error {{}}
)cpp");
TU.ExtraArgs.push_back("-Xclang");
TU.ExtraArgs.push_back("-verify");
EXPECT_THAT(*TU.build().getDiagnostics(), IsEmpty());
}
// Recursive main-file include is diagnosed, and doesn't crash.
TEST(DiagnosticsTest, RecursivePreamble) {
auto TU = TestTU::withCode(R"cpp(
#include "foo.h" // error-ok
int symbol;
)cpp");
TU.Filename = "foo.h";
EXPECT_THAT(*TU.build().getDiagnostics(),
ElementsAre(DiagName("pp_including_mainfile_in_preamble")));
EXPECT_THAT(TU.build().getLocalTopLevelDecls(), SizeIs(1));
}
// Recursive main-file include with #pragma once guard is OK.
TEST(DiagnosticsTest, RecursivePreamblePragmaOnce) {
auto TU = TestTU::withCode(R"cpp(
#pragma once
#include "foo.h"
int symbol;
)cpp");
TU.Filename = "foo.h";
EXPECT_THAT(*TU.build().getDiagnostics(), IsEmpty());
EXPECT_THAT(TU.build().getLocalTopLevelDecls(), SizeIs(1));
}
// Recursive main-file include with #ifndef guard should be OK.
// However, it's not yet recognized (incomplete at end of preamble).
TEST(DiagnosticsTest, RecursivePreambleIfndefGuard) {
auto TU = TestTU::withCode(R"cpp(
#ifndef FOO
#define FOO
#include "foo.h" // error-ok
int symbol;
#endif
)cpp");
TU.Filename = "foo.h";
// FIXME: should be no errors here.
EXPECT_THAT(*TU.build().getDiagnostics(),
ElementsAre(DiagName("pp_including_mainfile_in_preamble")));
EXPECT_THAT(TU.build().getLocalTopLevelDecls(), SizeIs(1));
}
TEST(DiagnosticsTest, InsideMacros) {
Annotations Test(R"cpp(
#define TEN 10
#define RET(x) return x + 10
int* foo() {
RET($foo[[0]]); // error-ok
}
int* bar() {
return $bar[[TEN]];
}
)cpp");
EXPECT_THAT(*TestTU::withCode(Test.code()).build().getDiagnostics(),
ElementsAre(Diag(Test.range("foo"),
"cannot initialize return object of type "
"'int *' with an rvalue of type 'int'"),
Diag(Test.range("bar"),
"cannot initialize return object of type "
"'int *' with an rvalue of type 'int'")));
}
TEST(DiagnosticsTest, NoFixItInMacro) {
Annotations Test(R"cpp(
#define Define(name) void name() {}
[[Define]](main) // error-ok
)cpp");
auto TU = TestTU::withCode(Test.code());
EXPECT_THAT(*TU.build().getDiagnostics(),
ElementsAre(AllOf(Diag(Test.range(), "'main' must return 'int'"),
Not(WithFix(_)))));
}
TEST(ClangdTest, MSAsm) {
// Parsing MS assembly tries to use the target MCAsmInfo, which we don't link.
// We used to crash here. Now clang emits a diagnostic, which we filter out.
llvm::InitializeAllTargetInfos(); // As in ClangdMain
auto TU = TestTU::withCode("void fn() { __asm { cmp cl,64 } }");
TU.ExtraArgs = {"-fms-extensions"};
EXPECT_THAT(*TU.build().getDiagnostics(), IsEmpty());
}
TEST(DiagnosticsTest, ToLSP) {
URIForFile MainFile =
URIForFile::canonicalize(testPath("foo/bar/main.cpp"), "");
URIForFile HeaderFile =
URIForFile::canonicalize(testPath("foo/bar/header.h"), "");
clangd::Diag D;
D.ID = clang::diag::err_undeclared_var_use;
D.Tags = {DiagnosticTag::Unnecessary};
D.Name = "undeclared_var_use";
D.Source = clangd::Diag::Clang;
D.Message = "something terrible happened";
D.Range = {pos(1, 2), pos(3, 4)};
D.InsideMainFile = true;
D.Severity = DiagnosticsEngine::Error;
D.File = "foo/bar/main.cpp";
D.AbsFile = std::string(MainFile.file());
clangd::Note NoteInMain;
NoteInMain.Message = "declared somewhere in the main file";
NoteInMain.Range = {pos(5, 6), pos(7, 8)};
NoteInMain.Severity = DiagnosticsEngine::Remark;
NoteInMain.File = "../foo/bar/main.cpp";
NoteInMain.InsideMainFile = true;
NoteInMain.AbsFile = std::string(MainFile.file());
D.Notes.push_back(NoteInMain);
clangd::Note NoteInHeader;
NoteInHeader.Message = "declared somewhere in the header file";
NoteInHeader.Range = {pos(9, 10), pos(11, 12)};
NoteInHeader.Severity = DiagnosticsEngine::Note;
NoteInHeader.File = "../foo/baz/header.h";
NoteInHeader.InsideMainFile = false;
NoteInHeader.AbsFile = std::string(HeaderFile.file());
D.Notes.push_back(NoteInHeader);
clangd::Fix F;
F.Message = "do something";
D.Fixes.push_back(F);
// Diagnostics should turn into these:
clangd::Diagnostic MainLSP;
MainLSP.range = D.Range;
MainLSP.severity = getSeverity(DiagnosticsEngine::Error);
MainLSP.code = "undeclared_var_use";
MainLSP.source = "clang";
MainLSP.message =
R"(Something terrible happened (fix available)
main.cpp:6:7: remark: declared somewhere in the main file
../foo/baz/header.h:10:11:
note: declared somewhere in the header file)";
MainLSP.tags = {DiagnosticTag::Unnecessary};
clangd::Diagnostic NoteInMainLSP;
NoteInMainLSP.range = NoteInMain.Range;
NoteInMainLSP.severity = getSeverity(DiagnosticsEngine::Remark);
NoteInMainLSP.message = R"(Declared somewhere in the main file
main.cpp:2:3: error: something terrible happened)";
ClangdDiagnosticOptions Opts;
// Transform diagnostics and check the results.
std::vector<std::pair<clangd::Diagnostic, std::vector<clangd::Fix>>> LSPDiags;
toLSPDiags(D, MainFile, Opts,
[&](clangd::Diagnostic LSPDiag, ArrayRef<clangd::Fix> Fixes) {
LSPDiags.push_back(
{std::move(LSPDiag),
std::vector<clangd::Fix>(Fixes.begin(), Fixes.end())});
});
EXPECT_THAT(
LSPDiags,
ElementsAre(Pair(EqualToLSPDiag(MainLSP), ElementsAre(EqualToFix(F))),
Pair(EqualToLSPDiag(NoteInMainLSP), IsEmpty())));
EXPECT_EQ(LSPDiags[0].first.code, "undeclared_var_use");
EXPECT_EQ(LSPDiags[0].first.source, "clang");
EXPECT_EQ(LSPDiags[1].first.code, "");
EXPECT_EQ(LSPDiags[1].first.source, "");
// Same thing, but don't flatten notes into the main list.
LSPDiags.clear();
Opts.EmitRelatedLocations = true;
toLSPDiags(D, MainFile, Opts,
[&](clangd::Diagnostic LSPDiag, ArrayRef<clangd::Fix> Fixes) {
LSPDiags.push_back(
{std::move(LSPDiag),
std::vector<clangd::Fix>(Fixes.begin(), Fixes.end())});
});
MainLSP.message = "Something terrible happened (fix available)";
DiagnosticRelatedInformation NoteInMainDRI;
NoteInMainDRI.message = "Declared somewhere in the main file";
NoteInMainDRI.location.range = NoteInMain.Range;
NoteInMainDRI.location.uri = MainFile;
MainLSP.relatedInformation = {NoteInMainDRI};
DiagnosticRelatedInformation NoteInHeaderDRI;
NoteInHeaderDRI.message = "Declared somewhere in the header file";
NoteInHeaderDRI.location.range = NoteInHeader.Range;
NoteInHeaderDRI.location.uri = HeaderFile;
MainLSP.relatedInformation = {NoteInMainDRI, NoteInHeaderDRI};
EXPECT_THAT(LSPDiags, ElementsAre(Pair(EqualToLSPDiag(MainLSP),
ElementsAre(EqualToFix(F)))));
}
struct SymbolWithHeader {
std::string QName;
std::string DeclaringFile;
std::string IncludeHeader;
};
std::unique_ptr<SymbolIndex>
buildIndexWithSymbol(llvm::ArrayRef<SymbolWithHeader> Syms) {
SymbolSlab::Builder Slab;
for (const auto &S : Syms) {
Symbol Sym = cls(S.QName);
Sym.Flags |= Symbol::IndexedForCodeCompletion;
Sym.CanonicalDeclaration.FileURI = S.DeclaringFile.c_str();
Sym.Definition.FileURI = S.DeclaringFile.c_str();
Sym.IncludeHeaders.emplace_back(S.IncludeHeader, 1);
Slab.insert(Sym);
}
return MemIndex::build(std::move(Slab).build(), RefSlab(), RelationSlab());
}
TEST(IncludeFixerTest, IncompleteType) {
Annotations Test(R"cpp(// error-ok
$insert[[]]namespace ns {
class X;
$nested[[X::]]Nested n;
}
class Y : $base[[public ns::X]] {};
void test(ns::X *x, ns::X& ref_x) {
x$access[[->]]f();
auto& $type[[[]]a] = *x;
ns::X $incomplete[[var]];
$tag[[ref_x]]->f();
$use[[ns::X()]];
$sizeof[[sizeof]](ns::X);
for (auto it : $for[[ref_x]]);
}
ns::X $return[[func]]() {}
class T {
ns::X $field[[x]];
};
)cpp");
auto TU = TestTU::withCode(Test.code());
TU.ExtraArgs.push_back("-std=c++17");
auto Index = buildIndexWithSymbol(
{SymbolWithHeader{"ns::X", "unittest:///x.h", "\"x.h\""}});
TU.ExternalIndex = Index.get();
EXPECT_THAT(
*TU.build().getDiagnostics(),
UnorderedElementsAreArray(
{AllOf(Diag(Test.range("nested"),
"incomplete type 'ns::X' named in nested name specifier"),
DiagName("incomplete_nested_name_spec"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("base"), "base class has incomplete type"),
DiagName("incomplete_base_class"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("access"),
"member access into incomplete type 'ns::X'"),
DiagName("incomplete_member_access"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(
Diag(
Test.range("type"),
"incomplete type 'ns::X' where a complete type is required"),
DiagName("incomplete_type"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("incomplete"),
"variable has incomplete type 'ns::X'"),
DiagName("typecheck_decl_incomplete_type"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(
Diag(Test.range("tag"), "incomplete definition of type 'ns::X'"),
DiagName("typecheck_incomplete_tag"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("use"),
"invalid use of incomplete type 'ns::X'"),
DiagName("invalid_incomplete_type_use"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("sizeof"),
"invalid application of 'sizeof' to "
"an incomplete type 'ns::X'"),
DiagName("sizeof_alignof_incomplete_or_sizeless_type"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("for"),
"cannot use incomplete type 'ns::X' as a range"),
DiagName("for_range_incomplete_type"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("return"),
"incomplete result type 'ns::X' in function definition"),
DiagName("func_def_incomplete_result"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("field"), "field has incomplete type 'ns::X'"),
DiagName("field_incomplete_or_sizeless"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X")))}))
<< Test.code();
}
TEST(IncludeFixerTest, NoSuggestIncludeWhenNoDefinitionInHeader) {
Annotations Test(R"cpp(// error-ok
$insert[[]]namespace ns {
class X;
}
class Y : $base[[public ns::X]] {};
int main() {
ns::X *x;
x$access[[->]]f();
}
)cpp");
auto TU = TestTU::withCode(Test.code());
Symbol Sym = cls("ns::X");
Sym.Flags |= Symbol::IndexedForCodeCompletion;
Sym.CanonicalDeclaration.FileURI = "unittest:///x.h";
Sym.Definition.FileURI = "unittest:///x.cc";
Sym.IncludeHeaders.emplace_back("\"x.h\"", 1);
SymbolSlab::Builder Slab;
Slab.insert(Sym);
auto Index =
MemIndex::build(std::move(Slab).build(), RefSlab(), RelationSlab());
TU.ExternalIndex = Index.get();
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(
Diag(Test.range("base"), "base class has incomplete type"),
Diag(Test.range("access"),
"member access into incomplete type 'ns::X'")));
}
TEST(IncludeFixerTest, Typo) {
Annotations Test(R"cpp(// error-ok
$insert[[]]namespace ns {
void foo() {
$unqualified1[[X]] x;
// No fix if the unresolved type is used as specifier. (ns::)X::Nested will be
// considered the unresolved type.
$unqualified2[[X]]::Nested n;
}
}
void bar() {
ns::$qualified1[[X]] x; // ns:: is valid.
ns::$qualified2[[X]](); // Error: no member in namespace
::$global[[Global]] glob;
}
using Type = ns::$template[[Foo]]<int>;
)cpp");
auto TU = TestTU::withCode(Test.code());
auto Index = buildIndexWithSymbol(
{SymbolWithHeader{"ns::X", "unittest:///x.h", "\"x.h\""},
SymbolWithHeader{"Global", "unittest:///global.h", "\"global.h\""},
SymbolWithHeader{"ns::Foo", "unittest:///foo.h", "\"foo.h\""}});
TU.ExternalIndex = Index.get();
EXPECT_THAT(
*TU.build().getDiagnostics(),
UnorderedElementsAre(
AllOf(Diag(Test.range("unqualified1"), "unknown type name 'X'"),
DiagName("unknown_typename"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
Diag(Test.range("unqualified2"), "use of undeclared identifier 'X'"),
AllOf(Diag(Test.range("qualified1"),
"no type named 'X' in namespace 'ns'"),
DiagName("typename_nested_not_found"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("qualified2"),
"no member named 'X' in namespace 'ns'"),
DiagName("no_member"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("global"),
"no type named 'Global' in the global namespace"),
DiagName("typename_nested_not_found"),
WithFix(Fix(Test.range("insert"), "#include \"global.h\"\n",
"Add include \"global.h\" for symbol Global"))),
AllOf(Diag(Test.range("template"),
"no template named 'Foo' in namespace 'ns'"),
DiagName("no_member_template"),
WithFix(Fix(Test.range("insert"), "#include \"foo.h\"\n",
"Add include \"foo.h\" for symbol ns::Foo")))));
}
TEST(IncludeFixerTest, MultipleMatchedSymbols) {
Annotations Test(R"cpp(// error-ok
$insert[[]]namespace na {
namespace nb {
void foo() {
$unqualified[[X]] x;
}
}
}
)cpp");
auto TU = TestTU::withCode(Test.code());
auto Index = buildIndexWithSymbol(
{SymbolWithHeader{"na::X", "unittest:///a.h", "\"a.h\""},
SymbolWithHeader{"na::nb::X", "unittest:///b.h", "\"b.h\""}});
TU.ExternalIndex = Index.get();
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(AllOf(
Diag(Test.range("unqualified"), "unknown type name 'X'"),
DiagName("unknown_typename"),
WithFix(Fix(Test.range("insert"), "#include \"a.h\"\n",
"Add include \"a.h\" for symbol na::X"),
Fix(Test.range("insert"), "#include \"b.h\"\n",
"Add include \"b.h\" for symbol na::nb::X")))));
}
TEST(IncludeFixerTest, NoCrashMemebrAccess) {
Annotations Test(R"cpp(// error-ok
struct X { int xyz; };
void g() { X x; x.$[[xy]]; }
)cpp");
auto TU = TestTU::withCode(Test.code());
auto Index = buildIndexWithSymbol(
SymbolWithHeader{"na::X", "unittest:///a.h", "\"a.h\""});
TU.ExternalIndex = Index.get();
EXPECT_THAT(
*TU.build().getDiagnostics(),
UnorderedElementsAre(Diag(Test.range(), "no member named 'xy' in 'X'")));
}
TEST(IncludeFixerTest, UseCachedIndexResults) {
// As index results for the identical request are cached, more than 5 fixes
// are generated.
Annotations Test(R"cpp(// error-ok
$insert[[]]void foo() {
$x1[[X]] x;
$x2[[X]] x;
$x3[[X]] x;
$x4[[X]] x;
$x5[[X]] x;
$x6[[X]] x;
$x7[[X]] x;
}
class X;
void bar(X *x) {
x$a1[[->]]f();
x$a2[[->]]f();
x$a3[[->]]f();
x$a4[[->]]f();
x$a5[[->]]f();
x$a6[[->]]f();
x$a7[[->]]f();
}
)cpp");
auto TU = TestTU::withCode(Test.code());
auto Index =
buildIndexWithSymbol(SymbolWithHeader{"X", "unittest:///a.h", "\"a.h\""});
TU.ExternalIndex = Index.get();
auto Parsed = TU.build();
for (const auto &D : *Parsed.getDiagnostics()) {
if (D.Fixes.size() != 1) {
ADD_FAILURE() << "D.Fixes.size() != 1";
continue;
}
EXPECT_EQ(D.Fixes[0].Message,
std::string("Add include \"a.h\" for symbol X"));
}
}
TEST(IncludeFixerTest, UnresolvedNameAsSpecifier) {
Annotations Test(R"cpp(// error-ok
$insert[[]]namespace ns {
}
void g() { ns::$[[scope]]::X_Y(); }
)cpp");
TestTU TU;
TU.Code = std::string(Test.code());
// FIXME: Figure out why this is needed and remove it, PR43662.
TU.ExtraArgs.push_back("-fno-ms-compatibility");
auto Index = buildIndexWithSymbol(
SymbolWithHeader{"ns::scope::X_Y", "unittest:///x.h", "\"x.h\""});
TU.ExternalIndex = Index.get();
EXPECT_THAT(
*TU.build().getDiagnostics(),
UnorderedElementsAre(AllOf(
Diag(Test.range(), "no member named 'scope' in namespace 'ns'"),
DiagName("no_member"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::scope::X_Y")))));
}
TEST(IncludeFixerTest, UnresolvedSpecifierWithSemaCorrection) {
Annotations Test(R"cpp(// error-ok
$insert[[]]namespace clang {
void f() {
// "clangd::" will be corrected to "clang::" by Sema.
$q1[[clangd]]::$x[[X]] x;
$q2[[clangd]]::$ns[[ns]]::Y y;
}
}
)cpp");
TestTU TU;
TU.Code = std::string(Test.code());
// FIXME: Figure out why this is needed and remove it, PR43662.
TU.ExtraArgs.push_back("-fno-ms-compatibility");
auto Index = buildIndexWithSymbol(
{SymbolWithHeader{"clang::clangd::X", "unittest:///x.h", "\"x.h\""},
SymbolWithHeader{"clang::clangd::ns::Y", "unittest:///y.h", "\"y.h\""}});
TU.ExternalIndex = Index.get();
EXPECT_THAT(
*TU.build().getDiagnostics(),
UnorderedElementsAre(
AllOf(
Diag(Test.range("q1"), "use of undeclared identifier 'clangd'; "
"did you mean 'clang'?"),
DiagName("undeclared_var_use_suggest"),
WithFix(_, // change clangd to clang
Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol clang::clangd::X"))),
AllOf(
Diag(Test.range("x"), "no type named 'X' in namespace 'clang'"),
DiagName("typename_nested_not_found"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol clang::clangd::X"))),
AllOf(
Diag(Test.range("q2"), "use of undeclared identifier 'clangd'; "
"did you mean 'clang'?"),
DiagName("undeclared_var_use_suggest"),
WithFix(
_, // change clangd to clang
Fix(Test.range("insert"), "#include \"y.h\"\n",
"Add include \"y.h\" for symbol clang::clangd::ns::Y"))),
AllOf(Diag(Test.range("ns"),
"no member named 'ns' in namespace 'clang'"),
DiagName("no_member"),
WithFix(Fix(
Test.range("insert"), "#include \"y.h\"\n",
"Add include \"y.h\" for symbol clang::clangd::ns::Y")))));
}
TEST(IncludeFixerTest, SpecifiedScopeIsNamespaceAlias) {
Annotations Test(R"cpp(// error-ok
$insert[[]]namespace a {}
namespace b = a;
namespace c {
b::$[[X]] x;
}
)cpp");
auto TU = TestTU::withCode(Test.code());
auto Index = buildIndexWithSymbol(
SymbolWithHeader{"a::X", "unittest:///x.h", "\"x.h\""});
TU.ExternalIndex = Index.get();
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(AllOf(
Diag(Test.range(), "no type named 'X' in namespace 'a'"),
DiagName("typename_nested_not_found"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol a::X")))));
}
TEST(IncludeFixerTest, NoCrashOnTemplateInstantiations) {
Annotations Test(R"cpp(
template <typename T> struct Templ {
template <typename U>
typename U::type operator=(const U &);
};
struct A {
Templ<char> s;
A() { [[a]]; /*error-ok*/ } // crash if we compute scopes lazily.
};
)cpp");
auto TU = TestTU::withCode(Test.code());
auto Index = buildIndexWithSymbol({});
TU.ExternalIndex = Index.get();
EXPECT_THAT(
*TU.build().getDiagnostics(),
ElementsAre(Diag(Test.range(), "use of undeclared identifier 'a'")));
}
TEST(DiagsInHeaders, DiagInsideHeader) {
Annotations Main(R"cpp(
#include [["a.h"]]
void foo() {})cpp");
Annotations Header("[[no_type_spec]]; // error-ok");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {{"a.h", std::string(Header.code())}};
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(AllOf(
Diag(Main.range(), "in included file: C++ requires a "
"type specifier for all declarations"),
WithNote(Diag(Header.range(), "error occurred here")))));
}
TEST(DiagsInHeaders, DiagInTransitiveInclude) {
Annotations Main(R"cpp(
#include [["a.h"]]
void foo() {})cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {{"a.h", "#include \"b.h\""},
{"b.h", "no_type_spec; // error-ok"}};
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(
Diag(Main.range(), "in included file: C++ requires a "
"type specifier for all declarations")));
}
TEST(DiagsInHeaders, DiagInMultipleHeaders) {
Annotations Main(R"cpp(
#include $a[["a.h"]]
#include $b[["b.h"]]
void foo() {})cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {{"a.h", "no_type_spec; // error-ok"},
{"b.h", "no_type_spec; // error-ok"}};
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(
Diag(Main.range("a"), "in included file: C++ requires a type "
"specifier for all declarations"),
Diag(Main.range("b"), "in included file: C++ requires a type "
"specifier for all declarations")));
}
TEST(DiagsInHeaders, PreferExpansionLocation) {
Annotations Main(R"cpp(
#include [["a.h"]]
#include "b.h"
void foo() {})cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {
{"a.h", "#include \"b.h\"\n"},
{"b.h", "#ifndef X\n#define X\nno_type_spec; // error-ok\n#endif"}};
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(Diag(Main.range(),
"in included file: C++ requires a type "
"specifier for all declarations")));
}
TEST(DiagsInHeaders, PreferExpansionLocationMacros) {
Annotations Main(R"cpp(
#define X
#include "a.h"
#undef X
#include [["b.h"]]
void foo() {})cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {
{"a.h", "#include \"c.h\"\n"},
{"b.h", "#include \"c.h\"\n"},
{"c.h", "#ifndef X\n#define X\nno_type_spec; // error-ok\n#endif"}};
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(
Diag(Main.range(), "in included file: C++ requires a "
"type specifier for all declarations")));
}
TEST(DiagsInHeaders, LimitDiagsOutsideMainFile) {
Annotations Main(R"cpp(
#include [["a.h"]]
#include "b.h"
void foo() {})cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {{"a.h", "#include \"c.h\"\n"},
{"b.h", "#include \"c.h\"\n"},
{"c.h", R"cpp(
#ifndef X
#define X
no_type_spec_0; // error-ok
no_type_spec_1;
no_type_spec_2;
no_type_spec_3;
no_type_spec_4;
no_type_spec_5;
no_type_spec_6;
no_type_spec_7;
no_type_spec_8;
no_type_spec_9;
no_type_spec_10;
#endif)cpp"}};
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(
Diag(Main.range(), "in included file: C++ requires a "
"type specifier for all declarations")));
}
TEST(DiagsInHeaders, OnlyErrorOrFatal) {
Annotations Main(R"cpp(
#include [["a.h"]]
void foo() {})cpp");
Annotations Header(R"cpp(
[[no_type_spec]]; // error-ok
int x = 5/0;)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {{"a.h", std::string(Header.code())}};
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(AllOf(
Diag(Main.range(), "in included file: C++ requires "
"a type specifier for all declarations"),
WithNote(Diag(Header.range(), "error occurred here")))));
}
TEST(DiagsInHeaders, OnlyDefaultErrorOrFatal) {
Annotations Main(R"cpp(
#include [["a.h"]] // get unused "foo" warning when building preamble.
)cpp");
Annotations Header(R"cpp(
namespace { void foo() {} }
void func() {foo();} ;)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {{"a.h", std::string(Header.code())}};
// promote warnings to errors.
TU.ExtraArgs = {"-Werror", "-Wunused"};
EXPECT_THAT(*TU.build().getDiagnostics(), IsEmpty());
}
TEST(DiagsInHeaders, FromNonWrittenSources) {
Annotations Main(R"cpp(
#include [["a.h"]]
void foo() {})cpp");
Annotations Header(R"cpp(
int x = 5/0;
int b = [[FOO]]; // error-ok)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {{"a.h", std::string(Header.code())}};
TU.ExtraArgs = {"-DFOO=NOOO"};
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(AllOf(
Diag(Main.range(),
"in included file: use of undeclared identifier 'NOOO'"),
WithNote(Diag(Header.range(), "error occurred here")))));
}
TEST(DiagsInHeaders, ErrorFromMacroExpansion) {
Annotations Main(R"cpp(
void bar() {
int fo; // error-ok
#include [["a.h"]]
})cpp");
Annotations Header(R"cpp(
#define X foo
X;)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {{"a.h", std::string(Header.code())}};
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(
Diag(Main.range(), "in included file: use of undeclared "
"identifier 'foo'; did you mean 'fo'?")));
}
TEST(DiagsInHeaders, ErrorFromMacroArgument) {
Annotations Main(R"cpp(
void bar() {
int fo; // error-ok
#include [["a.h"]]
})cpp");
Annotations Header(R"cpp(
#define X(arg) arg
X(foo);)cpp");
TestTU TU = TestTU::withCode(Main.code());
TU.AdditionalFiles = {{"a.h", std::string(Header.code())}};
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(
Diag(Main.range(), "in included file: use of undeclared "
"identifier 'foo'; did you mean 'fo'?")));
}
TEST(IgnoreDiags, FromNonWrittenInclude) {
TestTU TU;
TU.ExtraArgs.push_back("--include=a.h");
TU.AdditionalFiles = {{"a.h", "void main();"}};
// The diagnostic "main must return int" is from the header, we don't attempt
// to render it in the main file as there is no written location there.
EXPECT_THAT(*TU.build().getDiagnostics(), UnorderedElementsAre());
}
TEST(ToLSPDiag, RangeIsInMain) {
ClangdDiagnosticOptions Opts;
clangd::Diag D;
D.Range = {pos(1, 2), pos(3, 4)};
D.Notes.emplace_back();
Note &N = D.Notes.back();
N.Range = {pos(2, 3), pos(3, 4)};
D.InsideMainFile = true;
N.InsideMainFile = false;
toLSPDiags(D, {}, Opts,
[&](clangd::Diagnostic LSPDiag, ArrayRef<clangd::Fix>) {
EXPECT_EQ(LSPDiag.range, D.Range);
});
D.InsideMainFile = false;
N.InsideMainFile = true;
toLSPDiags(D, {}, Opts,
[&](clangd::Diagnostic LSPDiag, ArrayRef<clangd::Fix>) {
EXPECT_EQ(LSPDiag.range, N.Range);
});
}
TEST(ParsedASTTest, ModuleSawDiag) {
static constexpr const llvm::StringLiteral KDiagMsg = "StampedDiag";
struct DiagModifierModule final : public FeatureModule {
struct Listener : public FeatureModule::ASTListener {
void sawDiagnostic(const clang::Diagnostic &Info,
clangd::Diag &Diag) override {
Diag.Message = KDiagMsg.str();
}
};
std::unique_ptr<ASTListener> astListeners() override {
return std::make_unique<Listener>();
};
};
FeatureModuleSet FMS;
FMS.add(std::make_unique<DiagModifierModule>());
Annotations Code("[[test]]; /* error-ok */");
TestTU TU;
TU.Code = Code.code().str();
TU.FeatureModules = &FMS;
auto AST = TU.build();
EXPECT_THAT(*AST.getDiagnostics(),
testing::Contains(Diag(Code.range(), KDiagMsg.str())));
}
TEST(Preamble, EndsOnNonEmptyLine) {
TestTU TU;
TU.ExtraArgs = {"-Wnewline-eof"};
{
TU.Code = "#define FOO\n void bar();\n";
auto AST = TU.build();
EXPECT_THAT(*AST.getDiagnostics(), IsEmpty());
}
{
Annotations Code("#define FOO[[]]");
TU.Code = Code.code().str();
auto AST = TU.build();
EXPECT_THAT(
*AST.getDiagnostics(),
testing::Contains(Diag(Code.range(), "no newline at end of file")));
}
}
TEST(Diagnostics, Tags) {
TestTU TU;
TU.ExtraArgs = {"-Wunused", "-Wdeprecated"};
Annotations Test(R"cpp(
void bar() __attribute__((deprecated));
void foo() {
int $unused[[x]];
$deprecated[[bar]]();
})cpp");
TU.Code = Test.code().str();
EXPECT_THAT(*TU.build().getDiagnostics(),
UnorderedElementsAre(
AllOf(Diag(Test.range("unused"), "unused variable 'x'"),
WithTag(DiagnosticTag::Unnecessary)),
AllOf(Diag(Test.range("deprecated"), "'bar' is deprecated"),
WithTag(DiagnosticTag::Deprecated))));
}
TEST(DiagnosticsTest, IncludeCleaner) {
Annotations Test(R"cpp(
$fix[[ $diag[[#include "unused.h"]]
]]
#include "used.h"
#include <system_header.h>
void foo() {
used();
}
)cpp");
TestTU TU;
TU.Code = Test.code().str();
TU.AdditionalFiles["unused.h"] = R"cpp(
#pragma once
void unused() {}
)cpp";
TU.AdditionalFiles["used.h"] = R"cpp(
#pragma once
void used() {}
)cpp";
TU.AdditionalFiles["system/system_header.h"] = "";
TU.ExtraArgs = {"-isystem" + testPath("system")};
// Off by default.
EXPECT_THAT(*TU.build().getDiagnostics(), IsEmpty());
Config Cfg;
Cfg.Diagnostics.UnusedIncludes = Config::UnusedIncludesPolicy::Strict;
WithContextValue WithCfg(Config::Key, std::move(Cfg));
EXPECT_THAT(
*TU.build().getDiagnostics(),
UnorderedElementsAre(AllOf(
Diag(Test.range("diag"), "included header unused.h is not used"),
WithTag(DiagnosticTag::Unnecessary), DiagSource(Diag::Clangd),
WithFix(Fix(Test.range("fix"), "", "remove #include directive")))));
Cfg.Diagnostics.SuppressAll = true;
WithContextValue SuppressAllWithCfg(Config::Key, std::move(Cfg));
EXPECT_THAT(*TU.build().getDiagnostics(), IsEmpty());
Cfg.Diagnostics.SuppressAll = false;
Cfg.Diagnostics.Suppress = {"unused-includes"};
WithContextValue SuppressFilterWithCfg(Config::Key, std::move(Cfg));
EXPECT_THAT(*TU.build().getDiagnostics(), IsEmpty());
}
} // namespace
} // namespace clangd
} // namespace clang