diff options
Diffstat (limited to 'clangd/unittests/DiagnosticsTests.cpp')
-rw-r--r-- | clangd/unittests/DiagnosticsTests.cpp | 773 |
1 files changed, 773 insertions, 0 deletions
diff --git a/clangd/unittests/DiagnosticsTests.cpp b/clangd/unittests/DiagnosticsTests.cpp new file mode 100644 index 00000000..826428ca --- /dev/null +++ b/clangd/unittests/DiagnosticsTests.cpp @@ -0,0 +1,773 @@ +//===--- 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 "ClangdUnit.h" +#include "Diagnostics.h" +#include "Path.h" +#include "Protocol.h" +#include "SourceCode.h" +#include "TestFS.h" +#include "TestIndex.h" +#include "TestTU.h" +#include "index/MemIndex.h" +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/DiagnosticSema.h" +#include "llvm/Support/ScopedPrinter.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <algorithm> + +namespace clang { +namespace clangd { +namespace { + +using ::testing::_; +using ::testing::ElementsAre; +using ::testing::Field; +using ::testing::IsEmpty; +using ::testing::Pair; +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 &> +WithNote(::testing::Matcher<Note> NoteMatcher) { + return Field(&Diag::Notes, ElementsAre(NoteMatcher)); +} + +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(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(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; +} + +TEST(DiagnosticsTest, DiagnosticRanges) { + // Check we report correct ranges, including various edge-cases. + Annotations Test(R"cpp( + namespace test{}; + void $decl[[foo]](); + int main() { + $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]]; + } + )cpp"); + EXPECT_THAT( + TestTU::withCode(Test.code()).build().getDiagnostics(), + ElementsAre( + // 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\\ o' 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'"))); +} + +TEST(DiagnosticsTest, FlagsMatter) { + Annotations Test("[[void]] main() {}"); + 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"]] + )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, ClangTidy) { + Annotations Test(R"cpp( + #include $deprecated[["assert.h"]] + + #define $macrodef[[SQUARE]](X) (X)*(X) + int main() { + return $doubled[[sizeof]](sizeof(int)); + } + int square() { + int y = 4; + return SQUARE($macroarg[[++]]y); + } + )cpp"); + auto TU = TestTU::withCode(Test.code()); + TU.HeaderFilename = "assert.h"; // Suppress "not found" error. + TU.ClangTidyChecks = + "-*, bugprone-sizeof-expression, bugprone-macro-repeated-side-effects, " + "modernize-deprecated-headers"; + EXPECT_THAT( + TU.build().getDiagnostics(), + 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"))), + Diag(Test.range("macroarg"), + "multiple unsequenced modifications to 'y'"))); +} + +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]]; + #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, InsideMacros) { + Annotations Test(R"cpp( + #define TEN 10 + #define RET(x) return x + 10 + + int* foo() { + RET($foo[[0]]); + } + 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) + )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, 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_enum_class_reference; + D.Name = "enum_class_reference"; + 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 = 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 = 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 = 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 = "enum_class_reference"; + 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)"; + + 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, "enum_class_reference"); + 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()); +} + +TEST(IncludeFixerTest, IncompleteType) { + Annotations Test(R"cpp( +$insert[[]]namespace ns { + class X; + $nested[[X::]]Nested n; +} +class Y : $base[[public ns::X]] {}; +int main() { + ns::X *x; + x$access[[->]]f(); +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + {SymbolWithHeader{"ns::X", "unittest:///x.h", "\"x.h\""}}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre( + AllOf(Diag(Test.range("nested"), + "incomplete type 'ns::X' named in nested name specifier"), + 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"), + 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'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))))); +} + +TEST(IncludeFixerTest, NoSuggestIncludeWhenNoDefinitionInHeader) { + Annotations Test(R"cpp( +$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()); + 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( +$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; +} + )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\""}}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre( + AllOf(Diag(Test.range("unqualified1"), "unknown type name 'X'"), + 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'"), + 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'"), + 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"), + WithFix(Fix(Test.range("insert"), "#include \"global.h\"\n", + "Add include \"global.h\" for symbol Global"))))); +} + +TEST(IncludeFixerTest, MultipleMatchedSymbols) { + Annotations Test(R"cpp( +$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'"), + 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( + 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( +$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()) { + EXPECT_EQ(D.Fixes.size(), 1u); + EXPECT_EQ(D.Fixes[0].Message, + std::string("Add include \"a.h\" for symbol X")); + } +} + +TEST(IncludeFixerTest, UnresolvedNameAsSpecifier) { + Annotations Test(R"cpp( +$insert[[]]namespace ns { +} +void g() { ns::$[[scope]]::X_Y(); } + )cpp"); + auto TU = TestTU::withCode(Test.code()); + 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'"), + 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( +$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"); + auto TU = TestTU::withCode(Test.code()); + 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'?"), + 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'"), + 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'?"), + WithFix( + _, // change clangd to clangd + 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'"), + 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( +$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'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol a::X"))))); +} + +TEST(DiagsInHeaders, DiagInsideHeader) { + Annotations Main(R"cpp( + #include [["a.h"]] + void foo() {})cpp"); + Annotations Header("[[no_type_spec]];"); + TestTU TU = TestTU::withCode(Main.code()); + TU.AdditionalFiles = {{"a.h", 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;"}}; + 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;"}, {"b.h", "no_type_spec;"}}; + 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;\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;\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; + 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]]; + int x = 5/0;)cpp"); + TestTU TU = TestTU::withCode(Main.code()); + TU.AdditionalFiles = {{"a.h", Header.code()}}; + auto diags = TU.build().getDiagnostics(); + 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"))))); +} +} // namespace + +} // namespace clangd +} // namespace clang |