mirror of
https://github.com/llvm/llvm-project.git
synced 2025-04-18 14:56:44 +00:00

Clangd does not support its checker because the checker relies on having seen preprocessor conditionals that occur in the preamble, and clangd does not currently replay those. This checker was already disabled under its main name, modernize-macro-to-enum (https://github.com/clangd/clangd/issues/1464). This commit disables it under the alternative name cppcoreguidelines-macro-to-enum as well. Fixes https://github.com/llvm/llvm-project/issues/127965
2157 lines
77 KiB
C++
2157 lines
77 KiB
C++
//===--- 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 "../clang-tidy/ClangTidyOptions.h"
|
|
#include "Annotations.h"
|
|
#include "Config.h"
|
|
#include "Diagnostics.h"
|
|
#include "Feature.h"
|
|
#include "FeatureModule.h"
|
|
#include "ParsedAST.h"
|
|
#include "Protocol.h"
|
|
#include "TestFS.h"
|
|
#include "TestIndex.h"
|
|
#include "TestTU.h"
|
|
#include "TidyProvider.h"
|
|
#include "index/MemIndex.h"
|
|
#include "index/Ref.h"
|
|
#include "index/Relation.h"
|
|
#include "index/Symbol.h"
|
|
#include "support/Context.h"
|
|
#include "support/Path.h"
|
|
#include "clang/AST/Decl.h"
|
|
#include "clang/Basic/Diagnostic.h"
|
|
#include "clang/Basic/DiagnosticSema.h"
|
|
#include "clang/Basic/LLVM.h"
|
|
#include "clang/Basic/Specifiers.h"
|
|
#include "llvm/ADT/ArrayRef.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "llvm/Support/JSON.h"
|
|
#include "llvm/Support/ScopedPrinter.h"
|
|
#include "llvm/Support/TargetSelect.h"
|
|
#include "llvm/Testing/Support/SupportHelpers.h"
|
|
#include "gmock/gmock.h"
|
|
#include "gtest/gtest.h"
|
|
#include <cstddef>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace clang {
|
|
namespace clangd {
|
|
namespace {
|
|
|
|
using ::testing::_;
|
|
using ::testing::AllOf;
|
|
using ::testing::Contains;
|
|
using ::testing::Each;
|
|
using ::testing::ElementsAre;
|
|
using ::testing::Field;
|
|
using ::testing::IsEmpty;
|
|
using ::testing::Not;
|
|
using ::testing::Pair;
|
|
using ::testing::SizeIs;
|
|
using ::testing::UnorderedElementsAre;
|
|
|
|
::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 &> withID(unsigned ID) {
|
|
return Field(&Diag::ID, ID);
|
|
}
|
|
::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_P(hasRange, Range, "") { return arg.Range == Range; }
|
|
|
|
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);
|
|
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, CoroutineInHeader) {
|
|
StringRef CoroutineH = R"cpp(
|
|
namespace std {
|
|
template <class Ret, typename... T>
|
|
struct coroutine_traits { using promise_type = typename Ret::promise_type; };
|
|
|
|
template <class Promise = void>
|
|
struct coroutine_handle {
|
|
static coroutine_handle from_address(void *) noexcept;
|
|
static coroutine_handle from_promise(Promise &promise);
|
|
constexpr void* address() const noexcept;
|
|
};
|
|
template <>
|
|
struct coroutine_handle<void> {
|
|
template <class PromiseType>
|
|
coroutine_handle(coroutine_handle<PromiseType>) noexcept;
|
|
static coroutine_handle from_address(void *);
|
|
constexpr void* address() const noexcept;
|
|
};
|
|
|
|
struct awaitable {
|
|
bool await_ready() noexcept { return false; }
|
|
void await_suspend(coroutine_handle<>) noexcept {}
|
|
void await_resume() noexcept {}
|
|
};
|
|
} // namespace std
|
|
)cpp";
|
|
|
|
StringRef Header = R"cpp(
|
|
#include "coroutine.h"
|
|
template <typename T> struct [[clang::coro_return_type]] Gen {
|
|
struct promise_type {
|
|
Gen<T> get_return_object() {
|
|
return {};
|
|
}
|
|
std::awaitable initial_suspend();
|
|
std::awaitable final_suspend() noexcept;
|
|
void unhandled_exception();
|
|
void return_value(T t);
|
|
};
|
|
};
|
|
|
|
Gen<int> foo_coro(int b) { co_return b; }
|
|
)cpp";
|
|
Annotations Main(R"cpp(
|
|
// error-ok
|
|
#include "header.hpp"
|
|
Gen<int> $[[bar_coro]](int b) { return foo_coro(b); }
|
|
)cpp");
|
|
TestTU TU = TestTU::withCode(Main.code());
|
|
TU.AdditionalFiles["coroutine.h"] = std::string(CoroutineH);
|
|
TU.AdditionalFiles["header.hpp"] = std::string(Header);
|
|
TU.ExtraArgs.push_back("--std=c++20");
|
|
EXPECT_THAT(TU.build().getDiagnostics(), ElementsAre(hasRange(Main.range())));
|
|
}
|
|
|
|
TEST(DiagnosticTest, MakeShared) {
|
|
// We usually miss diagnostics from header functions as we don't parse them.
|
|
// std::make_shared is only parsed when --parse-forwarding-functions is set
|
|
Annotations Main(R"cpp(
|
|
struct S { S(char*); };
|
|
auto x = std::[[make_shared]]<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 shared_ptr for simplicity.
|
|
// forward is included to show its body is not needed to get the diagnostic.
|
|
template <typename T> T&& forward(T& t);
|
|
template <typename T, typename... A> T* make_shared(A&&... args) {
|
|
return new T(std::forward<A>(args)...);
|
|
}
|
|
}
|
|
)cpp";
|
|
TU.ParseOpts.PreambleParseForwardingFunctions = true;
|
|
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-mismatch");
|
|
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.
|
|
// NOLINTBEGIN
|
|
}
|
|
)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, ClangTidySystemMacro) {
|
|
Annotations Main(R"cpp(
|
|
#include "user.h"
|
|
#include "system.h"
|
|
int i = 3;
|
|
double x = $inline[[8]] / i;
|
|
double y = $user[[DIVIDE_USER]](i);
|
|
double z = DIVIDE_SYS(i);
|
|
)cpp");
|
|
|
|
auto TU = TestTU::withCode(Main.code());
|
|
TU.AdditionalFiles["user.h"] = R"cpp(
|
|
#define DIVIDE_USER(Y) 8/Y
|
|
)cpp";
|
|
TU.AdditionalFiles["system.h"] = R"cpp(
|
|
#pragma clang system_header
|
|
#define DIVIDE_SYS(Y) 8/Y
|
|
)cpp";
|
|
|
|
TU.ClangTidyProvider = addTidyChecks("bugprone-integer-division");
|
|
std::string BadDivision = "result of integer division used in a floating "
|
|
"point context; possible loss of precision";
|
|
|
|
// Expect to see warning from user macros, but not system macros.
|
|
// This matches clang-tidy --system-headers=0 (the default).
|
|
EXPECT_THAT(TU.build().getDiagnostics(),
|
|
ifTidyChecks(
|
|
UnorderedElementsAre(Diag(Main.range("inline"), BadDivision),
|
|
Diag(Main.range("user"), BadDivision))));
|
|
}
|
|
|
|
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)))));
|
|
}
|
|
|
|
TidyProvider addClangArgs(std::vector<llvm::StringRef> ExtraArgs,
|
|
llvm::StringRef Checks) {
|
|
return [ExtraArgs = std::move(ExtraArgs), Checks = Checks.str()](
|
|
tidy::ClangTidyOptions &Opts, llvm::StringRef) {
|
|
if (!Opts.ExtraArgs)
|
|
Opts.ExtraArgs.emplace();
|
|
for (llvm::StringRef Arg : ExtraArgs)
|
|
Opts.ExtraArgs->emplace_back(Arg);
|
|
if (!Checks.empty())
|
|
Opts.Checks = Checks;
|
|
};
|
|
}
|
|
|
|
TEST(DiagnosticTest, ClangTidyEnablesClangWarning) {
|
|
Annotations Main(R"cpp( // error-ok
|
|
static void [[foo]]() {}
|
|
)cpp");
|
|
TestTU TU = TestTU::withCode(Main.code());
|
|
// This is always emitted as a clang warning, not a clang-tidy diagnostic.
|
|
auto UnusedFooWarning =
|
|
AllOf(Diag(Main.range(), "unused function 'foo'"),
|
|
diagName("-Wunused-function"), diagSource(Diag::Clang),
|
|
diagSeverity(DiagnosticsEngine::Warning));
|
|
|
|
// Check the -Wunused warning isn't initially on.
|
|
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
|
|
|
|
// We enable warnings based on clang-tidy extra args, if the matching
|
|
// clang-diagnostic- is there.
|
|
TU.ClangTidyProvider =
|
|
addClangArgs({"-Wunused"}, "clang-diagnostic-unused-function");
|
|
EXPECT_THAT(TU.build().getDiagnostics(), ElementsAre(UnusedFooWarning));
|
|
|
|
// clang-diagnostic-* is acceptable
|
|
TU.ClangTidyProvider = addClangArgs({"-Wunused"}, "clang-diagnostic-*");
|
|
EXPECT_THAT(TU.build().getDiagnostics(), ElementsAre(UnusedFooWarning));
|
|
// And plain * (may turn on other checks too).
|
|
TU.ClangTidyProvider = addClangArgs({"-Wunused"}, "*");
|
|
EXPECT_THAT(TU.build().getDiagnostics(), Contains(UnusedFooWarning));
|
|
// And we can explicitly exclude a category too.
|
|
TU.ClangTidyProvider = addClangArgs(
|
|
{"-Wunused"}, "clang-diagnostic-*,-clang-diagnostic-unused-function");
|
|
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
|
|
|
|
// Without the exact check specified, the warnings are not enabled.
|
|
TU.ClangTidyProvider = addClangArgs({"-Wunused"}, "clang-diagnostic-unused");
|
|
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
|
|
|
|
// We don't respect other args.
|
|
TU.ClangTidyProvider = addClangArgs({"-Wunused", "-Dfoo=bar"},
|
|
"clang-diagnostic-unused-function");
|
|
EXPECT_THAT(TU.build().getDiagnostics(), ElementsAre(UnusedFooWarning))
|
|
<< "Not unused function 'bar'!";
|
|
|
|
// -Werror doesn't apply to warnings enabled by clang-tidy extra args.
|
|
TU.ExtraArgs = {"-Werror"};
|
|
TU.ClangTidyProvider =
|
|
addClangArgs({"-Wunused"}, "clang-diagnostic-unused-function");
|
|
EXPECT_THAT(TU.build().getDiagnostics(),
|
|
ElementsAre(diagSeverity(DiagnosticsEngine::Warning)));
|
|
|
|
// But clang-tidy extra args won't *downgrade* errors to warnings either.
|
|
TU.ExtraArgs = {"-Wunused", "-Werror"};
|
|
TU.ClangTidyProvider =
|
|
addClangArgs({"-Wunused"}, "clang-diagnostic-unused-function");
|
|
EXPECT_THAT(TU.build().getDiagnostics(),
|
|
ElementsAre(diagSeverity(DiagnosticsEngine::Error)));
|
|
|
|
// FIXME: we're erroneously downgrading the whole group, this should be Error.
|
|
TU.ExtraArgs = {"-Wunused-function", "-Werror"};
|
|
TU.ClangTidyProvider =
|
|
addClangArgs({"-Wunused"}, "clang-diagnostic-unused-label");
|
|
EXPECT_THAT(TU.build().getDiagnostics(),
|
|
ElementsAre(diagSeverity(DiagnosticsEngine::Warning)));
|
|
|
|
// This looks silly, but it's the typical result if a warning is enabled by a
|
|
// high-level .clang-tidy file and disabled by a low-level one.
|
|
TU.ExtraArgs = {};
|
|
TU.ClangTidyProvider = addClangArgs({"-Wunused", "-Wno-unused"},
|
|
"clang-diagnostic-unused-function");
|
|
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
|
|
|
|
// Overriding only works in the proper order.
|
|
TU.ClangTidyProvider =
|
|
addClangArgs({"-Wunused"}, {"clang-diagnostic-unused-function"});
|
|
EXPECT_THAT(TU.build().getDiagnostics(), SizeIs(1));
|
|
|
|
// More specific vs less-specific: match clang behavior
|
|
TU.ClangTidyProvider = addClangArgs({"-Wunused", "-Wno-unused-function"},
|
|
{"clang-diagnostic-unused-function"});
|
|
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
|
|
TU.ClangTidyProvider = addClangArgs({"-Wunused-function", "-Wno-unused"},
|
|
{"clang-diagnostic-unused-function"});
|
|
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
|
|
|
|
// We do allow clang-tidy config to disable warnings from the compile
|
|
// command. It's unclear this is ideal, but it's hard to avoid.
|
|
TU.ExtraArgs = {"-Wunused"};
|
|
TU.ClangTidyProvider = addClangArgs({"-Wno-unused"}, {});
|
|
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
|
|
|
|
TU.ExtraArgs = {"-Wno-unused"};
|
|
TU.ClangTidyProvider = addClangArgs({"-Wunused"}, {"-*, clang-diagnostic-*"});
|
|
EXPECT_THAT(TU.build().getDiagnostics(), SizeIs(1));
|
|
}
|
|
|
|
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, NewLineFixMessage) {
|
|
Annotations Source("int a;[[]]");
|
|
TestTU TU = TestTU::withCode(Source.code());
|
|
TU.ExtraArgs = {"-Wnewline-eof"};
|
|
EXPECT_THAT(
|
|
TU.build().getDiagnostics(),
|
|
ElementsAre(withFix((Fix(Source.range(), "\n", "insert '\\n'")))));
|
|
}
|
|
|
|
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, ClangTidyMacroToEnumCheck) {
|
|
Annotations Main(R"cpp(
|
|
#if 1
|
|
auto foo();
|
|
#endif
|
|
)cpp");
|
|
TestTU TU = TestTU::withCode(Main.code());
|
|
std::vector<TidyProvider> Providers;
|
|
Providers.push_back(
|
|
addTidyChecks("cppcoreguidelines-macro-to-enum,modernize-macro-to-enum"));
|
|
Providers.push_back(disableUnusableChecks());
|
|
TU.ClangTidyProvider = combine(std::move(Providers));
|
|
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(DiagnosticTest, ClangTidySelfContainedDiags) {
|
|
Annotations Main(R"cpp($MathHeader[[]]
|
|
struct Foo{
|
|
int A, B;
|
|
Foo()$Fix[[]] {
|
|
$A[[A = 1;]]
|
|
$B[[B = 1;]]
|
|
}
|
|
};
|
|
void InitVariables() {
|
|
float $C[[C]]$CFix[[]];
|
|
double $D[[D]]$DFix[[]];
|
|
}
|
|
)cpp");
|
|
TestTU TU = TestTU::withCode(Main.code());
|
|
TU.ClangTidyProvider =
|
|
addTidyChecks("cppcoreguidelines-prefer-member-initializer,"
|
|
"cppcoreguidelines-init-variables");
|
|
clangd::Fix ExpectedAFix;
|
|
ExpectedAFix.Message =
|
|
"'A' should be initialized in a member initializer of the constructor";
|
|
ExpectedAFix.Edits.push_back(TextEdit{Main.range("Fix"), " : A(1)"});
|
|
ExpectedAFix.Edits.push_back(TextEdit{Main.range("A"), ""});
|
|
|
|
// When invoking clang-tidy normally, this code would produce `, B(1)` as the
|
|
// fix the `B` member, as it would think its already included the ` : ` from
|
|
// the previous `A` fix.
|
|
clangd::Fix ExpectedBFix;
|
|
ExpectedBFix.Message =
|
|
"'B' should be initialized in a member initializer of the constructor";
|
|
ExpectedBFix.Edits.push_back(TextEdit{Main.range("Fix"), " : B(1)"});
|
|
ExpectedBFix.Edits.push_back(TextEdit{Main.range("B"), ""});
|
|
|
|
clangd::Fix ExpectedCFix;
|
|
ExpectedCFix.Message = "variable 'C' is not initialized";
|
|
ExpectedCFix.Edits.push_back(TextEdit{Main.range("CFix"), " = NAN"});
|
|
ExpectedCFix.Edits.push_back(
|
|
TextEdit{Main.range("MathHeader"), "#include <math.h>\n\n"});
|
|
|
|
// Again in clang-tidy only the include directive would be emitted for the
|
|
// first warning. However we need the include attaching for both warnings.
|
|
clangd::Fix ExpectedDFix;
|
|
ExpectedDFix.Message = "variable 'D' is not initialized";
|
|
ExpectedDFix.Edits.push_back(TextEdit{Main.range("DFix"), " = NAN"});
|
|
ExpectedDFix.Edits.push_back(
|
|
TextEdit{Main.range("MathHeader"), "#include <math.h>\n\n"});
|
|
EXPECT_THAT(
|
|
TU.build().getDiagnostics(),
|
|
ifTidyChecks(UnorderedElementsAre(
|
|
AllOf(Diag(Main.range("A"), "'A' should be initialized in a member "
|
|
"initializer of the constructor"),
|
|
withFix(equalToFix(ExpectedAFix))),
|
|
AllOf(Diag(Main.range("B"), "'B' should be initialized in a member "
|
|
"initializer of the constructor"),
|
|
withFix(equalToFix(ExpectedBFix))),
|
|
AllOf(Diag(Main.range("C"), "variable 'C' is not initialized"),
|
|
withFix(equalToFix(ExpectedCFix))),
|
|
AllOf(Diag(Main.range("D"), "variable 'D' is not initialized"),
|
|
withFix(equalToFix(ExpectedDFix))))));
|
|
}
|
|
|
|
TEST(DiagnosticTest, ClangTidySelfContainedDiagsFormatting) {
|
|
Annotations Main(R"cpp(
|
|
class Interface {
|
|
public:
|
|
virtual void Reset1() = 0;
|
|
virtual void Reset2() = 0;
|
|
};
|
|
class A : public Interface {
|
|
// This will be marked by clangd to use override instead of virtual
|
|
$virtual1[[virtual ]]void $Reset1[[Reset1]]()$override1[[]];
|
|
$virtual2[[virtual ]]/**/void $Reset2[[Reset2]]()$override2[[]];
|
|
};
|
|
)cpp");
|
|
TestTU TU = TestTU::withCode(Main.code());
|
|
TU.ClangTidyProvider =
|
|
addTidyChecks("cppcoreguidelines-explicit-virtual-functions,");
|
|
clangd::Fix const ExpectedFix1{
|
|
"prefer using 'override' or (rarely) 'final' "
|
|
"instead of 'virtual'",
|
|
{TextEdit{Main.range("override1"), " override"},
|
|
TextEdit{Main.range("virtual1"), ""}},
|
|
{}};
|
|
clangd::Fix const ExpectedFix2{
|
|
"prefer using 'override' or (rarely) 'final' "
|
|
"instead of 'virtual'",
|
|
{TextEdit{Main.range("override2"), " override"},
|
|
TextEdit{Main.range("virtual2"), ""}},
|
|
{}};
|
|
// Note that in the Fix we expect the "virtual" keyword and the following
|
|
// whitespace to be deleted
|
|
EXPECT_THAT(TU.build().getDiagnostics(),
|
|
ifTidyChecks(UnorderedElementsAre(
|
|
AllOf(Diag(Main.range("Reset1"),
|
|
"prefer using 'override' or (rarely) 'final' "
|
|
"instead of 'virtual'"),
|
|
withFix(equalToFix(ExpectedFix1))),
|
|
AllOf(Diag(Main.range("Reset2"),
|
|
"prefer using 'override' or (rarely) 'final' "
|
|
"instead of 'virtual'"),
|
|
withFix(equalToFix(ExpectedFix2))))));
|
|
}
|
|
|
|
TEST(DiagnosticsTest, ClangTidyCallingIntoPreprocessor) {
|
|
std::string Main = R"cpp(
|
|
extern "C" {
|
|
#include "b.h"
|
|
}
|
|
)cpp";
|
|
std::string Header = R"cpp(
|
|
#define EXTERN extern
|
|
EXTERN int waldo();
|
|
)cpp";
|
|
auto TU = TestTU::withCode(Main);
|
|
TU.AdditionalFiles["b.h"] = Header;
|
|
TU.ClangTidyProvider = addTidyChecks("modernize-use-trailing-return-type");
|
|
// Check that no assertion failures occur during the build
|
|
TU.build();
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
TEST(DiagnosticTest, IgnoreBEFilelistOptions) {
|
|
auto TU = TestTU::withCode("");
|
|
TU.ExtraArgs.push_back("-Xclang");
|
|
for (const auto *DisableOption :
|
|
{"-fsanitize-ignorelist=null", "-fprofile-list=null",
|
|
"-fxray-always-instrument=null", "-fxray-never-instrument=null",
|
|
"-fxray-attr-list=null"}) {
|
|
TU.ExtraArgs.push_back(DisableOption);
|
|
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
|
|
TU.ExtraArgs.pop_back();
|
|
}
|
|
}
|
|
|
|
// 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(),
|
|
Not(Contains(diagName("pp_including_mainfile_in_preamble"))));
|
|
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, PreambleWithPragmaAssumeNonnull) {
|
|
auto TU = TestTU::withCode(R"cpp(
|
|
#pragma clang assume_nonnull begin
|
|
void foo(int *x);
|
|
#pragma clang assume_nonnull end
|
|
)cpp");
|
|
auto AST = TU.build();
|
|
EXPECT_THAT(AST.getDiagnostics(), IsEmpty());
|
|
const auto *X = cast<FunctionDecl>(findDecl(AST, "foo")).getParamDecl(0);
|
|
ASSERT_TRUE(X->getOriginalType()->getNullability() ==
|
|
NullabilityKind::NonNull);
|
|
}
|
|
|
|
TEST(DiagnosticsTest, PreambleHeaderWithBadPragmaAssumeNonnull) {
|
|
Annotations Header(R"cpp(
|
|
#pragma clang assume_nonnull begin // error-ok
|
|
void foo(int *X);
|
|
)cpp");
|
|
auto TU = TestTU::withCode(R"cpp(
|
|
#include "foo.h" // unterminated assume_nonnull should not affect bar.
|
|
void bar(int *Y);
|
|
)cpp");
|
|
TU.AdditionalFiles = {{"foo.h", std::string(Header.code())}};
|
|
auto AST = TU.build();
|
|
EXPECT_THAT(AST.getDiagnostics(),
|
|
ElementsAre(diagName("pp_eof_in_assume_nonnull")));
|
|
const auto *X = cast<FunctionDecl>(findDecl(AST, "foo")).getParamDecl(0);
|
|
ASSERT_TRUE(X->getOriginalType()->getNullability() ==
|
|
NullabilityKind::NonNull);
|
|
const auto *Y = cast<FunctionDecl>(findDecl(AST, "bar")).getParamDecl(0);
|
|
ASSERT_FALSE(Y->getOriginalType()->getNullability());
|
|
}
|
|
|
|
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(DiagnosticsTest, PragmaSystemHeader) {
|
|
Annotations Test("#pragma clang [[system_header]]\n");
|
|
auto TU = TestTU::withCode(Test.code());
|
|
EXPECT_THAT(
|
|
TU.build().getDiagnostics(),
|
|
ElementsAre(AllOf(
|
|
Diag(Test.range(), "#pragma system_header ignored in main file"))));
|
|
TU.Filename = "TestTU.h";
|
|
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
|
|
}
|
|
|
|
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());
|
|
D.OpaqueData["test"] = "bar";
|
|
|
|
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};
|
|
MainLSP.data = D.OpaqueData;
|
|
|
|
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, Symbol::Include);
|
|
Slab.insert(Sym);
|
|
}
|
|
return MemIndex::build(std::move(Slab).build(), RefSlab(), RelationSlab());
|
|
}
|
|
|
|
TEST(IncludeFixerTest, IncompleteType) {
|
|
auto TU = TestTU::withHeaderCode("namespace ns { class X; } ns::X *x;");
|
|
TU.ExtraArgs.push_back("-std=c++20");
|
|
auto Index = buildIndexWithSymbol(
|
|
{SymbolWithHeader{"ns::X", "unittest:///x.h", "\"x.h\""}});
|
|
TU.ExternalIndex = Index.get();
|
|
|
|
std::vector<std::pair<llvm::StringRef, llvm::StringRef>> Tests{
|
|
{"incomplete_nested_name_spec", "[[ns::X::]]Nested n;"},
|
|
{"incomplete_base_class", "class Y : [[ns::X]] {};"},
|
|
{"incomplete_member_access", "auto i = x[[->]]f();"},
|
|
{"incomplete_type", "auto& [[[]]m] = *x;"},
|
|
{"init_incomplete_type",
|
|
"struct C { static int f(ns::X&); }; int i = C::f([[{]]});"},
|
|
{"bad_cast_incomplete", "auto a = [[static_cast]]<ns::X>(0);"},
|
|
{"template_nontype_parm_incomplete", "template <ns::X [[foo]]> int a;"},
|
|
{"typecheck_decl_incomplete_type", "ns::X [[var]];"},
|
|
{"typecheck_incomplete_tag", "auto i = [[(*x)]]->f();"},
|
|
{"typecheck_nonviable_condition_incomplete",
|
|
"struct A { operator ns::X(); } a; const ns::X &[[b]] = a;"},
|
|
{"invalid_incomplete_type_use", "auto var = [[ns::X()]];"},
|
|
{"sizeof_alignof_incomplete_or_sizeless_type",
|
|
"auto s = [[sizeof]](ns::X);"},
|
|
{"for_range_incomplete_type", "void foo() { for (auto i : [[*]]x ) {} }"},
|
|
{"func_def_incomplete_result", "ns::X [[func]] () {}"},
|
|
{"field_incomplete_or_sizeless", "class M { ns::X [[member]]; };"},
|
|
{"array_incomplete_or_sizeless_type", "auto s = [[(ns::X[]){}]];"},
|
|
{"call_incomplete_return", "ns::X f(); auto fp = &f; auto z = [[fp()]];"},
|
|
{"call_function_incomplete_return", "ns::X foo(); auto a = [[foo()]];"},
|
|
{"call_incomplete_argument", "int m(ns::X); int i = m([[*x]]);"},
|
|
{"switch_incomplete_class_type", "void a() { [[switch]](*x) {} }"},
|
|
{"delete_incomplete_class_type", "void f() { [[delete]] *x; }"},
|
|
{"-Wdelete-incomplete", "void f() { [[delete]] x; }"},
|
|
{"dereference_incomplete_type",
|
|
R"cpp(void f() { asm("" : "=r"([[*]]x)::); })cpp"},
|
|
};
|
|
for (auto Case : Tests) {
|
|
Annotations Main(Case.second);
|
|
TU.Code = Main.code().str() + "\n // error-ok";
|
|
EXPECT_THAT(
|
|
TU.build().getDiagnostics(),
|
|
ElementsAre(AllOf(diagName(Case.first), hasRange(Main.range()),
|
|
withFix(Fix(Range{}, "#include \"x.h\"\n",
|
|
"Include \"x.h\" for symbol ns::X")))))
|
|
<< Case.second;
|
|
}
|
|
}
|
|
|
|
TEST(IncludeFixerTest, IncompleteEnum) {
|
|
Symbol Sym = enm("X");
|
|
Sym.Flags |= Symbol::IndexedForCodeCompletion;
|
|
Sym.CanonicalDeclaration.FileURI = Sym.Definition.FileURI = "unittest:///x.h";
|
|
Sym.IncludeHeaders.emplace_back("\"x.h\"", 1, Symbol::Include);
|
|
SymbolSlab::Builder Slab;
|
|
Slab.insert(Sym);
|
|
auto Index =
|
|
MemIndex::build(std::move(Slab).build(), RefSlab(), RelationSlab());
|
|
|
|
TestTU TU;
|
|
TU.ExternalIndex = Index.get();
|
|
TU.ExtraArgs.push_back("-std=c++20");
|
|
TU.ExtraArgs.push_back("-fno-ms-compatibility"); // else incomplete enum is OK
|
|
|
|
std::vector<std::pair<llvm::StringRef, llvm::StringRef>> Tests{
|
|
{"incomplete_enum", "enum class X : int; using enum [[X]];"},
|
|
{"underlying_type_of_incomplete_enum",
|
|
"[[__underlying_type]](enum X) i;"},
|
|
};
|
|
for (auto Case : Tests) {
|
|
Annotations Main(Case.second);
|
|
TU.Code = Main.code().str() + "\n // error-ok";
|
|
EXPECT_THAT(TU.build().getDiagnostics(),
|
|
Contains(AllOf(diagName(Case.first), hasRange(Main.range()),
|
|
withFix(Fix(Range{}, "#include \"x.h\"\n",
|
|
"Include \"x.h\" for symbol X")))))
|
|
<< Case.second;
|
|
}
|
|
}
|
|
|
|
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, Symbol::Include);
|
|
|
|
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;
|
|
}
|
|
struct S : $base[[X]] {};
|
|
}
|
|
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",
|
|
"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",
|
|
"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",
|
|
"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",
|
|
"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",
|
|
"Include \"foo.h\" for symbol ns::Foo"))),
|
|
AllOf(Diag(Test.range("base"), "expected class name"),
|
|
diagName("expected_class_name"),
|
|
withFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
|
|
"Include \"x.h\" for symbol ns::X")))));
|
|
}
|
|
|
|
TEST(IncludeFixerTest, TypoInMacro) {
|
|
auto TU = TestTU::withCode(R"cpp(// error-ok
|
|
#define ID(T) T
|
|
X a1;
|
|
ID(X a2);
|
|
ns::X a3;
|
|
ID(ns::X a4);
|
|
namespace ns{};
|
|
ns::X a5;
|
|
ID(ns::X a6);
|
|
)cpp");
|
|
auto Index = buildIndexWithSymbol(
|
|
{SymbolWithHeader{"X", "unittest:///x.h", "\"x.h\""},
|
|
SymbolWithHeader{"ns::X", "unittest:///ns.h", "\"x.h\""}});
|
|
TU.ExternalIndex = Index.get();
|
|
// FIXME: -fms-compatibility (which is default on windows) breaks the
|
|
// ns::X cases when the namespace is undeclared. Find out why!
|
|
TU.ExtraArgs = {"-fno-ms-compatibility"};
|
|
EXPECT_THAT(TU.build().getDiagnostics(), Each(withFix(_)));
|
|
}
|
|
|
|
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",
|
|
"Include \"a.h\" for symbol na::X"),
|
|
Fix(Test.range("insert"), "#include \"b.h\"\n",
|
|
"Include \"b.h\" for symbol na::nb::X")))));
|
|
}
|
|
|
|
TEST(IncludeFixerTest, NoCrashMemberAccess) {
|
|
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("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",
|
|
"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",
|
|
"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",
|
|
"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",
|
|
"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",
|
|
"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",
|
|
"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(IncludeFixerTest, HeaderNamedInDiag) {
|
|
Annotations Test(R"cpp(
|
|
$insert[[]]int main() {
|
|
[[printf]]("");
|
|
}
|
|
)cpp");
|
|
auto TU = TestTU::withCode(Test.code());
|
|
TU.ExtraArgs = {"-xc", "-std=c99",
|
|
"-Wno-error=implicit-function-declaration"};
|
|
auto Index = buildIndexWithSymbol({});
|
|
TU.ExternalIndex = Index.get();
|
|
|
|
EXPECT_THAT(
|
|
TU.build().getDiagnostics(),
|
|
ElementsAre(AllOf(
|
|
Diag(Test.range(), "call to undeclared library function 'printf' "
|
|
"with type 'int (const char *, ...)'; ISO C99 "
|
|
"and later do not support implicit function "
|
|
"declarations"),
|
|
withFix(Fix(Test.range("insert"), "#include <stdio.h>\n",
|
|
"Include <stdio.h> for symbol printf")))));
|
|
|
|
TU.ExtraArgs = {"-xc", "-std=c89"};
|
|
EXPECT_THAT(
|
|
TU.build().getDiagnostics(),
|
|
ElementsAre(AllOf(
|
|
Diag(Test.range(), "implicitly declaring library function 'printf' "
|
|
"with type 'int (const char *, ...)'"),
|
|
withFix(Fix(Test.range("insert"), "#include <stdio.h>\n",
|
|
"Include <stdio.h> for symbol printf")))));
|
|
}
|
|
|
|
TEST(IncludeFixerTest, CImplicitFunctionDecl) {
|
|
Annotations Test("void x() { [[foo]](); }");
|
|
auto TU = TestTU::withCode(Test.code());
|
|
TU.Filename = "test.c";
|
|
TU.ExtraArgs = {"-std=c99", "-Wno-error=implicit-function-declaration"};
|
|
|
|
Symbol Sym = func("foo");
|
|
Sym.Flags |= Symbol::IndexedForCodeCompletion;
|
|
Sym.CanonicalDeclaration.FileURI = "unittest:///foo.h";
|
|
Sym.IncludeHeaders.emplace_back("\"foo.h\"", 1, Symbol::Include);
|
|
|
|
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(),
|
|
ElementsAre(AllOf(
|
|
Diag(Test.range(),
|
|
"call to undeclared function 'foo'; ISO C99 and later do not "
|
|
"support implicit function declarations"),
|
|
withFix(Fix(Range{}, "#include \"foo.h\"\n",
|
|
"Include \"foo.h\" for symbol foo")))));
|
|
|
|
TU.ExtraArgs = {"-std=c89", "-Wall"};
|
|
EXPECT_THAT(TU.build().getDiagnostics(),
|
|
ElementsAre(AllOf(
|
|
Diag(Test.range(), "implicit declaration of function 'foo'"),
|
|
withFix(Fix(Range{}, "#include \"foo.h\"\n",
|
|
"Include \"foo.h\" for symbol foo")))));
|
|
}
|
|
|
|
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: a type specifier is "
|
|
"required 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: a type specifier is "
|
|
"required 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: a type specifier is "
|
|
"required for all declarations"),
|
|
Diag(Main.range("b"), "in included file: a type specifier is "
|
|
"required 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(),
|
|
Contains(Diag(Main.range(), "in included file: a type specifier "
|
|
"is required 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: a type specifier is "
|
|
"required 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: a type specifier is "
|
|
"required 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: a type specifier is "
|
|
"required 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) {
|
|
TestTU TU;
|
|
|
|
auto AST = TU.build();
|
|
#if 0
|
|
EXPECT_THAT(AST.getDiagnostics(),
|
|
testing::Contains(Diag(Code.range(), KDiagMsg.str())));
|
|
#endif
|
|
}
|
|
|
|
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 = Annotations(R"cpp(
|
|
$typedef[[typedef int INT]];
|
|
)cpp");
|
|
TU.Code = Test.code();
|
|
TU.ClangTidyProvider = addTidyChecks("modernize-use-using");
|
|
EXPECT_THAT(
|
|
TU.build().getDiagnostics(),
|
|
ifTidyChecks(UnorderedElementsAre(
|
|
AllOf(Diag(Test.range("typedef"), "use 'using' instead of 'typedef'"),
|
|
withTag(DiagnosticTag::Deprecated)))));
|
|
}
|
|
|
|
TEST(Diagnostics, TidyDiagsArentAffectedFromWerror) {
|
|
TestTU TU;
|
|
TU.ExtraArgs = {"-Werror"};
|
|
Annotations Test(R"cpp($typedef[[typedef int INT]]; // error-ok)cpp");
|
|
TU.Code = Test.code().str();
|
|
TU.ClangTidyProvider = addTidyChecks("modernize-use-using");
|
|
EXPECT_THAT(
|
|
TU.build().getDiagnostics(),
|
|
ifTidyChecks(UnorderedElementsAre(
|
|
AllOf(Diag(Test.range("typedef"), "use 'using' instead of 'typedef'"),
|
|
// Make sure severity for clang-tidy finding isn't bumped to
|
|
// error due to Werror in compile flags.
|
|
diagSeverity(DiagnosticsEngine::Warning)))));
|
|
|
|
TU.ClangTidyProvider =
|
|
addTidyChecks("modernize-use-using", /*WarningsAsErrors=*/"modernize-*");
|
|
EXPECT_THAT(
|
|
TU.build().getDiagnostics(),
|
|
ifTidyChecks(UnorderedElementsAre(
|
|
AllOf(Diag(Test.range("typedef"), "use 'using' instead of 'typedef'"),
|
|
// Unless bumped explicitly with WarnAsError.
|
|
diagSeverity(DiagnosticsEngine::Error)))));
|
|
}
|
|
|
|
TEST(Diagnostics, DeprecatedDiagsAreHints) {
|
|
ClangdDiagnosticOptions Opts;
|
|
std::optional<clangd::Diagnostic> Diag;
|
|
clangd::Diag D;
|
|
D.Range = {pos(1, 2), pos(3, 4)};
|
|
D.InsideMainFile = true;
|
|
|
|
// Downgrade warnings with deprecated tags to remark.
|
|
D.Tags = {Deprecated};
|
|
D.Severity = DiagnosticsEngine::Warning;
|
|
toLSPDiags(D, {}, Opts,
|
|
[&](clangd::Diagnostic LSPDiag, ArrayRef<clangd::Fix>) {
|
|
Diag = std::move(LSPDiag);
|
|
});
|
|
EXPECT_EQ(Diag->severity, getSeverity(DiagnosticsEngine::Remark));
|
|
Diag.reset();
|
|
|
|
// Preserve errors.
|
|
D.Severity = DiagnosticsEngine::Error;
|
|
toLSPDiags(D, {}, Opts,
|
|
[&](clangd::Diagnostic LSPDiag, ArrayRef<clangd::Fix>) {
|
|
Diag = std::move(LSPDiag);
|
|
});
|
|
EXPECT_EQ(Diag->severity, getSeverity(DiagnosticsEngine::Error));
|
|
Diag.reset();
|
|
|
|
// No-op without tag.
|
|
D.Tags = {};
|
|
D.Severity = DiagnosticsEngine::Warning;
|
|
toLSPDiags(D, {}, Opts,
|
|
[&](clangd::Diagnostic LSPDiag, ArrayRef<clangd::Fix>) {
|
|
Diag = std::move(LSPDiag);
|
|
});
|
|
EXPECT_EQ(Diag->severity, getSeverity(DiagnosticsEngine::Warning));
|
|
}
|
|
|
|
TEST(DiagnosticsTest, IncludeCleaner) {
|
|
Annotations Test(R"cpp(
|
|
$fix[[ $diag[[#include "unused.h"]]
|
|
]]
|
|
#include "used.h"
|
|
|
|
#include "ignore.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["ignore.h"] = R"cpp(
|
|
#pragma once
|
|
void ignore() {}
|
|
)cpp";
|
|
TU.AdditionalFiles["system/system_header.h"] = "";
|
|
TU.ExtraArgs = {"-isystem" + testPath("system")};
|
|
Config Cfg;
|
|
Cfg.Diagnostics.UnusedIncludes = Config::IncludesPolicy::Strict;
|
|
// Set filtering.
|
|
Cfg.Diagnostics.Includes.IgnoreHeader.emplace_back(
|
|
[](llvm::StringRef Header) { return Header.ends_with("ignore.h"); });
|
|
WithContextValue WithCfg(Config::Key, std::move(Cfg));
|
|
auto AST = TU.build();
|
|
EXPECT_THAT(
|
|
AST.getDiagnostics(),
|
|
Contains(AllOf(
|
|
Diag(Test.range("diag"),
|
|
"included header unused.h is not used directly"),
|
|
withTag(DiagnosticTag::Unnecessary), diagSource(Diag::Clangd),
|
|
withFix(Fix(Test.range("fix"), "", "remove #include directive")))));
|
|
auto &Diag = AST.getDiagnostics().front();
|
|
EXPECT_THAT(getDiagnosticDocURI(Diag.Source, Diag.ID, Diag.Name),
|
|
llvm::ValueIs(Not(IsEmpty())));
|
|
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());
|
|
}
|
|
|
|
TEST(DiagnosticsTest, FixItFromHeader) {
|
|
llvm::StringLiteral Header(R"cpp(
|
|
void foo(int *);
|
|
void foo(int *, int);)cpp");
|
|
Annotations Source(R"cpp(
|
|
/*error-ok*/
|
|
void bar() {
|
|
int x;
|
|
$diag[[foo]]($fix[[]]x, 1);
|
|
})cpp");
|
|
TestTU TU;
|
|
TU.Code = Source.code().str();
|
|
TU.HeaderCode = Header.str();
|
|
EXPECT_THAT(
|
|
TU.build().getDiagnostics(),
|
|
UnorderedElementsAre(AllOf(
|
|
Diag(Source.range("diag"), "no matching function for call to 'foo'"),
|
|
withFix(Fix(Source.range("fix"), "&",
|
|
"candidate function not viable: no known conversion from "
|
|
"'int' to 'int *' for 1st argument; take the address of "
|
|
"the argument with &")))));
|
|
}
|
|
|
|
TEST(DiagnosticsTest, UnusedInHeader) {
|
|
// Clang diagnoses unused static inline functions outside headers.
|
|
auto TU = TestTU::withCode("static inline void foo(void) {}");
|
|
TU.ExtraArgs.push_back("-Wunused-function");
|
|
TU.Filename = "test.c";
|
|
EXPECT_THAT(TU.build().getDiagnostics(),
|
|
ElementsAre(withID(diag::warn_unused_function)));
|
|
// Sema should recognize a *.h file open in clangd as a header.
|
|
// https://github.com/clangd/vscode-clangd/issues/360
|
|
TU.Filename = "test.h";
|
|
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace clangd
|
|
} // namespace clang
|