| //===-- ClangdUnitTests.cpp - ClangdUnit tests ------------------*- C++ -*-===// | 
 | // | 
 | //                     The LLVM Compiler Infrastructure | 
 | // | 
 | // This file is distributed under the University of Illinois Open Source | 
 | // License. See LICENSE.TXT for details. | 
 | // | 
 | //===----------------------------------------------------------------------===// | 
 |  | 
 | #include "Annotations.h" | 
 | #include "ClangdUnit.h" | 
 | #include "SourceCode.h" | 
 | #include "TestTU.h" | 
 | #include "llvm/Support/ScopedPrinter.h" | 
 | #include "gmock/gmock.h" | 
 | #include "gtest/gtest.h" | 
 |  | 
 | namespace clang { | 
 | namespace clangd { | 
 | namespace { | 
 |  | 
 | 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 &> 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)) { | 
 |   return std::tie(arg.range, arg.severity, arg.message) == | 
 |          std::tie(LSPDiag.range, LSPDiag.severity, LSPDiag.message); | 
 | } | 
 |  | 
 | 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'?"), | 
 |                 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, ClangTidy) { | 
 |   Annotations Test(R"cpp( | 
 |     #include $deprecated[["assert.h"]] | 
 |  | 
 |     #define $macrodef[[SQUARE]](X) (X)*(X) | 
 |     int main() { | 
 |       return $doubled[[sizeof]](sizeof(int)); | 
 |       int y = 4; | 
 |       return SQUARE($macroarg[[++]]y); | 
 |     } | 
 |   )cpp"); | 
 |   auto TU = TestTU::withCode(Test.code()); | 
 |   TU.HeaderFilename = "assert.h"; // Suppress "not found" error. | 
 |   EXPECT_THAT( | 
 |       TU.build().getDiagnostics(), | 
 |       UnorderedElementsAre( | 
 |           AllOf(Diag(Test.range("deprecated"), | 
 |                      "inclusion of deprecated C++ header 'assert.h'; consider " | 
 |                      "using 'cassert' instead [modernize-deprecated-headers]"), | 
 |                 WithFix(Fix(Test.range("deprecated"), "<cassert>", | 
 |                             "change '\"assert.h\"' to '<cassert>'"))), | 
 |           Diag(Test.range("doubled"), | 
 |                "suspicious usage of 'sizeof(sizeof(...))' " | 
 |                "[bugprone-sizeof-expression]"), | 
 |           AllOf( | 
 |               Diag(Test.range("macroarg"), | 
 |                    "side effects in the 1st macro argument 'X' are repeated in " | 
 |                    "macro expansion [bugprone-macro-repeated-side-effects]"), | 
 |               WithNote(Diag(Test.range("macrodef"), | 
 |                             "macro 'SQUARE' defined here " | 
 |                             "[bugprone-macro-repeated-side-effects]"))), | 
 |           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, ToLSP) { | 
 |   clangd::Diag D; | 
 |   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"; | 
 |  | 
 |   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; | 
 |   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; | 
 |   D.Notes.push_back(NoteInHeader); | 
 |  | 
 |   clangd::Fix F; | 
 |   F.Message = "do something"; | 
 |   D.Fixes.push_back(F); | 
 |  | 
 |   auto MatchingLSP = [](const DiagBase &D, StringRef Message) { | 
 |     clangd::Diagnostic Res; | 
 |     Res.range = D.Range; | 
 |     Res.severity = getSeverity(D.Severity); | 
 |     Res.message = Message; | 
 |     return Res; | 
 |   }; | 
 |  | 
 |   // Diagnostics should turn into these: | 
 |   clangd::Diagnostic MainLSP = MatchingLSP(D, R"(Something terrible happened | 
 |  | 
 | 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 = | 
 |       MatchingLSP(NoteInMain, R"(Declared somewhere in the main file | 
 |  | 
 | main.cpp:2:3: error: something terrible happened)"); | 
 |  | 
 |   // Transform dianostics and check the results. | 
 |   std::vector<std::pair<clangd::Diagnostic, std::vector<clangd::Fix>>> LSPDiags; | 
 |   toLSPDiags(D, | 
 | #ifdef _WIN32 | 
 |              URIForFile::canonicalize("c:\\path\\to\\foo\\bar\\main.cpp", | 
 |                                       /*TUPath=*/""), | 
 | #else | 
 |       URIForFile::canonicalize("/path/to/foo/bar/main.cpp", /*TUPath=*/""), | 
 | #endif | 
 |              ClangdDiagnosticOptions(), | 
 |              [&](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()))); | 
 | } | 
 |  | 
 | TEST(ClangdUnitTest, GetBeginningOfIdentifier) { | 
 |   std::string Preamble = R"cpp( | 
 | struct Bar { int func(); }; | 
 | #define MACRO(X) void f() { X; } | 
 | Bar* bar; | 
 |   )cpp"; | 
 |   // First ^ is the expected beginning, last is the search position. | 
 |   for (std::string Text : std::vector<std::string>{ | 
 |            "int ^f^oo();", // inside identifier | 
 |            "int ^foo();",  // beginning of identifier | 
 |            "int ^foo^();", // end of identifier | 
 |            "int foo(^);",  // non-identifier | 
 |            "^int foo();",  // beginning of file (can't back up) | 
 |            "int ^f0^0();", // after a digit (lexing at N-1 is wrong) | 
 |            "int ^λλ^λ();", // UTF-8 handled properly when backing up | 
 |  | 
 |            // identifier in macro arg | 
 |            "MACRO(bar->^func())",  // beginning of identifier | 
 |            "MACRO(bar->^fun^c())", // inside identifier | 
 |            "MACRO(bar->^func^())", // end of identifier | 
 |            "MACRO(^bar->func())",  // begin identifier | 
 |            "MACRO(^bar^->func())", // end identifier | 
 |            "^MACRO(bar->func())",  // beginning of macro name | 
 |            "^MAC^RO(bar->func())", // inside macro name | 
 |            "^MACRO^(bar->func())", // end of macro name | 
 |        }) { | 
 |     std::string WithPreamble = Preamble + Text; | 
 |     Annotations TestCase(WithPreamble); | 
 |     auto AST = TestTU::withCode(TestCase.code()).build(); | 
 |     const auto &SourceMgr = AST.getASTContext().getSourceManager(); | 
 |     SourceLocation Actual = getBeginningOfIdentifier( | 
 |         AST, TestCase.points().back(), SourceMgr.getMainFileID()); | 
 |     Position ActualPos = offsetToPosition( | 
 |         TestCase.code(), | 
 |         SourceMgr.getFileOffset(SourceMgr.getSpellingLoc(Actual))); | 
 |     EXPECT_EQ(TestCase.points().front(), ActualPos) << Text; | 
 |   } | 
 | } | 
 |  | 
 | MATCHER_P(DeclNamed, Name, "") { | 
 |   if (NamedDecl *ND = dyn_cast<NamedDecl>(arg)) | 
 |     if (ND->getName() == Name) | 
 |       return true; | 
 |   if (auto *Stream = result_listener->stream()) { | 
 |     llvm::raw_os_ostream OS(*Stream); | 
 |     arg->dump(OS); | 
 |   } | 
 |   return false; | 
 | } | 
 |  | 
 | TEST(ClangdUnitTest, TopLevelDecls) { | 
 |   TestTU TU; | 
 |   TU.HeaderCode = R"( | 
 |     int header1(); | 
 |     int header2; | 
 |   )"; | 
 |   TU.Code = "int main();"; | 
 |   auto AST = TU.build(); | 
 |   EXPECT_THAT(AST.getLocalTopLevelDecls(), ElementsAre(DeclNamed("main"))); | 
 | } | 
 |  | 
 | } // namespace | 
 | } // namespace clangd | 
 | } // namespace clang |