diff --git a/clang-tools-extra/clangd/Diagnostics.cpp b/clang-tools-extra/clangd/Diagnostics.cpp index 919182d5f70c..7f1ab06db9d1 100644 --- a/clang-tools-extra/clangd/Diagnostics.cpp +++ b/clang-tools-extra/clangd/Diagnostics.cpp @@ -1,619 +1,623 @@ //===--- Diagnostics.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 "Diagnostics.h" #include "../clang-tidy/ClangTidyDiagnosticConsumer.h" #include "Compiler.h" #include "Logger.h" #include "Protocol.h" #include "SourceCode.h" #include "clang/Basic/AllDiagnostics.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/DiagnosticIDs.h" #include "clang/Basic/FileManager.h" #include "clang/Basic/SourceManager.h" #include "clang/Lex/Lexer.h" #include "clang/Lex/Token.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/Twine.h" #include "llvm/Support/Capacity.h" #include "llvm/Support/Path.h" #include "llvm/Support/ScopedPrinter.h" #include "llvm/Support/Signals.h" #include "llvm/Support/raw_ostream.h" #include #include namespace clang { namespace clangd { namespace { const char *getDiagnosticCode(unsigned ID) { switch (ID) { #define DIAG(ENUM, CLASS, DEFAULT_MAPPING, DESC, GROPU, SFINAE, NOWERROR, \ SHOWINSYSHEADER, CATEGORY) \ case clang::diag::ENUM: \ return #ENUM; #include "clang/Basic/DiagnosticASTKinds.inc" #include "clang/Basic/DiagnosticAnalysisKinds.inc" #include "clang/Basic/DiagnosticCommentKinds.inc" #include "clang/Basic/DiagnosticCommonKinds.inc" #include "clang/Basic/DiagnosticDriverKinds.inc" #include "clang/Basic/DiagnosticFrontendKinds.inc" #include "clang/Basic/DiagnosticLexKinds.inc" #include "clang/Basic/DiagnosticParseKinds.inc" #include "clang/Basic/DiagnosticRefactoringKinds.inc" #include "clang/Basic/DiagnosticSemaKinds.inc" #include "clang/Basic/DiagnosticSerializationKinds.inc" #undef DIAG default: return nullptr; } } bool mentionsMainFile(const Diag &D) { if (D.InsideMainFile) return true; // Fixes are always in the main file. if (!D.Fixes.empty()) return true; for (auto &N : D.Notes) { if (N.InsideMainFile) return true; } return false; } // Checks whether a location is within a half-open range. // Note that clang also uses closed source ranges, which this can't handle! bool locationInRange(SourceLocation L, CharSourceRange R, const SourceManager &M) { assert(R.isCharRange()); if (!R.isValid() || M.getFileID(R.getBegin()) != M.getFileID(R.getEnd()) || M.getFileID(R.getBegin()) != M.getFileID(L)) return false; return L != R.getEnd() && M.isPointWithin(L, R.getBegin(), R.getEnd()); } // Clang diags have a location (shown as ^) and 0 or more ranges (~~~~). // LSP needs a single range. Range diagnosticRange(const clang::Diagnostic &D, const LangOptions &L) { auto &M = D.getSourceManager(); auto Loc = M.getFileLoc(D.getLocation()); for (const auto &CR : D.getRanges()) { auto R = Lexer::makeFileCharRange(CR, M, L); if (locationInRange(Loc, R, M)) return halfOpenToRange(M, R); } // The range may be given as a fixit hint instead. for (const auto &F : D.getFixItHints()) { auto R = Lexer::makeFileCharRange(F.RemoveRange, M, L); if (locationInRange(Loc, R, M)) return halfOpenToRange(M, R); } // If the token at the location is not a comment, we use the token. // If we can't get the token at the location, fall back to using the location auto R = CharSourceRange::getCharRange(Loc); Token Tok; if (!Lexer::getRawToken(Loc, Tok, M, L, true) && Tok.isNot(tok::comment)) { R = CharSourceRange::getTokenRange(Tok.getLocation(), Tok.getEndLoc()); } return halfOpenToRange(M, R); } // Returns whether the \p D is modified. bool adjustDiagFromHeader(Diag &D, const clang::Diagnostic &Info, const LangOptions &LangOpts) { // We only report diagnostics with at least error severity from headers. if (D.Severity < DiagnosticsEngine::Level::Error) return false; const SourceLocation &DiagLoc = Info.getLocation(); const SourceManager &SM = Info.getSourceManager(); SourceLocation IncludeInMainFile; auto GetIncludeLoc = [&SM](SourceLocation SLoc) { return SM.getIncludeLoc(SM.getFileID(SLoc)); }; for (auto IncludeLocation = GetIncludeLoc(DiagLoc); IncludeLocation.isValid(); - IncludeLocation = GetIncludeLoc(IncludeLocation)) - IncludeInMainFile = IncludeLocation; + IncludeLocation = GetIncludeLoc(IncludeLocation)) { + if (clangd::isInsideMainFile(IncludeLocation, SM)) { + IncludeInMainFile = IncludeLocation; + break; + } + } if (IncludeInMainFile.isInvalid()) return false; // Update diag to point at include inside main file. D.File = SM.getFileEntryForID(SM.getMainFileID())->getName().str(); D.Range.start = sourceLocToPosition(SM, IncludeInMainFile); D.Range.end = sourceLocToPosition( SM, Lexer::getLocForEndOfToken(IncludeInMainFile, 0, SM, LangOpts)); D.InsideMainFile = true; // Add a note that will point to real diagnostic. const auto *FE = SM.getFileEntryForID(SM.getFileID(DiagLoc)); D.Notes.emplace_back(); Note &N = D.Notes.back(); N.AbsFile = FE->tryGetRealPathName(); N.File = FE->getName(); N.Message = "error occurred here"; N.Range = diagnosticRange(Info, LangOpts); // Update message to mention original file. D.Message = llvm::Twine("in included file: ", D.Message).str(); return true; } bool isInsideMainFile(const clang::Diagnostic &D) { if (!D.hasSourceManager()) return false; return clangd::isInsideMainFile(D.getLocation(), D.getSourceManager()); } bool isNote(DiagnosticsEngine::Level L) { return L == DiagnosticsEngine::Note || L == DiagnosticsEngine::Remark; } llvm::StringRef diagLeveltoString(DiagnosticsEngine::Level Lvl) { switch (Lvl) { case DiagnosticsEngine::Ignored: return "ignored"; case DiagnosticsEngine::Note: return "note"; case DiagnosticsEngine::Remark: return "remark"; case DiagnosticsEngine::Warning: return "warning"; case DiagnosticsEngine::Error: return "error"; case DiagnosticsEngine::Fatal: return "fatal error"; } llvm_unreachable("unhandled DiagnosticsEngine::Level"); } /// Prints a single diagnostic in a clang-like manner, the output includes /// location, severity and error message. An example of the output message is: /// /// main.cpp:12:23: error: undeclared identifier /// /// For main file we only print the basename and for all other files we print /// the filename on a separate line to provide a slightly more readable output /// in the editors: /// /// dir1/dir2/dir3/../../dir4/header.h:12:23 /// error: undeclared identifier void printDiag(llvm::raw_string_ostream &OS, const DiagBase &D) { if (D.InsideMainFile) { // Paths to main files are often taken from compile_command.json, where they // are typically absolute. To reduce noise we print only basename for them, // it should not be confusing and saves space. OS << llvm::sys::path::filename(D.File) << ":"; } else { OS << D.File << ":"; } // Note +1 to line and character. clangd::Range is zero-based, but when // printing for users we want one-based indexes. auto Pos = D.Range.start; OS << (Pos.line + 1) << ":" << (Pos.character + 1) << ":"; // The non-main-file paths are often too long, putting them on a separate // line improves readability. if (D.InsideMainFile) OS << " "; else OS << "\n"; OS << diagLeveltoString(D.Severity) << ": " << D.Message; } /// Capitalizes the first word in the diagnostic's message. std::string capitalize(std::string Message) { if (!Message.empty()) Message[0] = llvm::toUpper(Message[0]); return Message; } /// Returns a message sent to LSP for the main diagnostic in \p D. /// This message may include notes, if they're not emited in some other way. /// Example output: /// /// no matching function for call to 'foo' /// /// main.cpp:3:5: note: candidate function not viable: requires 2 arguments /// /// dir1/dir2/dir3/../../dir4/header.h:12:23 /// note: candidate function not viable: requires 3 arguments std::string mainMessage(const Diag &D, const ClangdDiagnosticOptions &Opts) { std::string Result; llvm::raw_string_ostream OS(Result); OS << D.Message; if (Opts.DisplayFixesCount && !D.Fixes.empty()) OS << " (" << (D.Fixes.size() > 1 ? "fixes" : "fix") << " available)"; // If notes aren't emitted as structured info, add them to the message. if (!Opts.EmitRelatedLocations) for (auto &Note : D.Notes) { OS << "\n\n"; printDiag(OS, Note); } OS.flush(); return capitalize(std::move(Result)); } /// Returns a message sent to LSP for the note of the main diagnostic. std::string noteMessage(const Diag &Main, const DiagBase &Note, const ClangdDiagnosticOptions &Opts) { std::string Result; llvm::raw_string_ostream OS(Result); OS << Note.Message; // If the client doesn't support structured links between the note and the // original diagnostic, then emit the main diagnostic to give context. if (!Opts.EmitRelatedLocations) { OS << "\n\n"; printDiag(OS, Main); } OS.flush(); return capitalize(std::move(Result)); } } // namespace llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const DiagBase &D) { OS << "["; if (!D.InsideMainFile) OS << D.File << ":"; OS << D.Range.start << "-" << D.Range.end << "] "; return OS << D.Message; } llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Fix &F) { OS << F.Message << " {"; const char *Sep = ""; for (const auto &Edit : F.Edits) { OS << Sep << Edit; Sep = ", "; } return OS << "}"; } llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Diag &D) { OS << static_cast(D); if (!D.Notes.empty()) { OS << ", notes: {"; const char *Sep = ""; for (auto &Note : D.Notes) { OS << Sep << Note; Sep = ", "; } OS << "}"; } if (!D.Fixes.empty()) { OS << ", fixes: {"; const char *Sep = ""; for (auto &Fix : D.Fixes) { OS << Sep << Fix; Sep = ", "; } } return OS; } CodeAction toCodeAction(const Fix &F, const URIForFile &File) { CodeAction Action; Action.title = F.Message; Action.kind = CodeAction::QUICKFIX_KIND; Action.edit.emplace(); Action.edit->changes.emplace(); (*Action.edit->changes)[File.uri()] = {F.Edits.begin(), F.Edits.end()}; return Action; } void toLSPDiags( const Diag &D, const URIForFile &File, const ClangdDiagnosticOptions &Opts, llvm::function_ref)> OutFn) { auto FillBasicFields = [](const DiagBase &D) -> clangd::Diagnostic { clangd::Diagnostic Res; Res.range = D.Range; Res.severity = getSeverity(D.Severity); return Res; }; clangd::Diagnostic Main = FillBasicFields(D); Main.code = D.Name; switch (D.Source) { case Diag::Clang: Main.source = "clang"; break; case Diag::ClangTidy: Main.source = "clang-tidy"; break; case Diag::Unknown: break; } if (Opts.EmbedFixesInDiagnostics) { Main.codeActions.emplace(); for (const auto &Fix : D.Fixes) Main.codeActions->push_back(toCodeAction(Fix, File)); } if (Opts.SendDiagnosticCategory && !D.Category.empty()) Main.category = D.Category; Main.message = mainMessage(D, Opts); if (Opts.EmitRelatedLocations) { Main.relatedInformation.emplace(); for (auto &Note : D.Notes) { if (!Note.AbsFile) { vlog("Dropping note from unknown file: {0}", Note); continue; } DiagnosticRelatedInformation RelInfo; RelInfo.location.range = Note.Range; RelInfo.location.uri = URIForFile::canonicalize(*Note.AbsFile, File.file()); RelInfo.message = noteMessage(D, Note, Opts); Main.relatedInformation->push_back(std::move(RelInfo)); } } OutFn(std::move(Main), D.Fixes); // If we didn't emit the notes as relatedLocations, emit separate diagnostics // so the user can find the locations easily. if (!Opts.EmitRelatedLocations) for (auto &Note : D.Notes) { if (!Note.InsideMainFile) continue; clangd::Diagnostic Res = FillBasicFields(Note); Res.message = noteMessage(D, Note, Opts); OutFn(std::move(Res), llvm::ArrayRef()); } } int getSeverity(DiagnosticsEngine::Level L) { switch (L) { case DiagnosticsEngine::Remark: return 4; case DiagnosticsEngine::Note: return 3; case DiagnosticsEngine::Warning: return 2; case DiagnosticsEngine::Fatal: case DiagnosticsEngine::Error: return 1; case DiagnosticsEngine::Ignored: return 0; } llvm_unreachable("Unknown diagnostic level!"); } std::vector StoreDiags::take(const clang::tidy::ClangTidyContext *Tidy) { // Fill in name/source now that we have all the context needed to map them. for (auto &Diag : Output) { if (const char *ClangDiag = getDiagnosticCode(Diag.ID)) { // Warnings controlled by -Wfoo are better recognized by that name. StringRef Warning = DiagnosticIDs::getWarningOptionForDiag(Diag.ID); if (!Warning.empty()) { Diag.Name = ("-W" + Warning).str(); } else { StringRef Name(ClangDiag); // Almost always an error, with a name like err_enum_class_reference. // Drop the err_ prefix for brevity. Name.consume_front("err_"); Diag.Name = Name; } Diag.Source = Diag::Clang; continue; } if (Tidy != nullptr) { std::string TidyDiag = Tidy->getCheckName(Diag.ID); if (!TidyDiag.empty()) { Diag.Name = std::move(TidyDiag); Diag.Source = Diag::ClangTidy; // clang-tidy bakes the name into diagnostic messages. Strip it out. // It would be much nicer to make clang-tidy not do this. auto CleanMessage = [&](std::string &Msg) { StringRef Rest(Msg); if (Rest.consume_back("]") && Rest.consume_back(Diag.Name) && Rest.consume_back(" [")) Msg.resize(Rest.size()); }; CleanMessage(Diag.Message); for (auto &Note : Diag.Notes) CleanMessage(Note.Message); for (auto &Fix : Diag.Fixes) CleanMessage(Fix.Message); continue; } } } // Deduplicate clang-tidy diagnostics -- some clang-tidy checks may emit // duplicated messages due to various reasons (e.g. the check doesn't handle // template instantiations well; clang-tidy alias checks). std::set> SeenDiags; llvm::erase_if(Output, [&](const Diag& D) { return !SeenDiags.emplace(D.Range, D.Message).second; }); return std::move(Output); } void StoreDiags::BeginSourceFile(const LangOptions &Opts, const Preprocessor *) { LangOpts = Opts; } void StoreDiags::EndSourceFile() { flushLastDiag(); LangOpts = None; } /// Sanitizes a piece for presenting it in a synthesized fix message. Ensures /// the result is not too large and does not contain newlines. static void writeCodeToFixMessage(llvm::raw_ostream &OS, llvm::StringRef Code) { constexpr unsigned MaxLen = 50; // Only show the first line if there are many. llvm::StringRef R = Code.split('\n').first; // Shorten the message if it's too long. R = R.take_front(MaxLen); OS << R; if (R.size() != Code.size()) OS << "…"; } void StoreDiags::HandleDiagnostic(DiagnosticsEngine::Level DiagLevel, const clang::Diagnostic &Info) { DiagnosticConsumer::HandleDiagnostic(DiagLevel, Info); if (!LangOpts || !Info.hasSourceManager()) { IgnoreDiagnostics::log(DiagLevel, Info); return; } bool InsideMainFile = isInsideMainFile(Info); SourceManager &SM = Info.getSourceManager(); auto FillDiagBase = [&](DiagBase &D) { D.Range = diagnosticRange(Info, *LangOpts); llvm::SmallString<64> Message; Info.FormatDiagnostic(Message); D.Message = Message.str(); D.InsideMainFile = InsideMainFile; D.File = SM.getFilename(Info.getLocation()); D.AbsFile = getCanonicalPath( SM.getFileEntryForID(SM.getFileID(Info.getLocation())), SM); D.Severity = DiagLevel; D.Category = DiagnosticIDs::getCategoryNameFromID( DiagnosticIDs::getCategoryNumberForDiag(Info.getID())) .str(); return D; }; auto AddFix = [&](bool SyntheticMessage) -> bool { assert(!Info.getFixItHints().empty() && "diagnostic does not have attached fix-its"); if (!InsideMainFile) return false; llvm::SmallVector Edits; for (auto &FixIt : Info.getFixItHints()) { // Follow clang's behavior, don't apply FixIt to the code in macros, // we are less certain it is the right fix. if (FixIt.RemoveRange.getBegin().isMacroID() || FixIt.RemoveRange.getEnd().isMacroID()) return false; if (!isInsideMainFile(FixIt.RemoveRange.getBegin(), SM)) return false; Edits.push_back(toTextEdit(FixIt, SM, *LangOpts)); } llvm::SmallString<64> Message; // If requested and possible, create a message like "change 'foo' to 'bar'". if (SyntheticMessage && Info.getNumFixItHints() == 1) { const auto &FixIt = Info.getFixItHint(0); bool Invalid = false; llvm::StringRef Remove = Lexer::getSourceText(FixIt.RemoveRange, SM, *LangOpts, &Invalid); llvm::StringRef Insert = FixIt.CodeToInsert; if (!Invalid) { llvm::raw_svector_ostream M(Message); if (!Remove.empty() && !Insert.empty()) { M << "change '"; writeCodeToFixMessage(M, Remove); M << "' to '"; writeCodeToFixMessage(M, Insert); M << "'"; } else if (!Remove.empty()) { M << "remove '"; writeCodeToFixMessage(M, Remove); M << "'"; } else if (!Insert.empty()) { M << "insert '"; writeCodeToFixMessage(M, Insert); M << "'"; } // Don't allow source code to inject newlines into diagnostics. std::replace(Message.begin(), Message.end(), '\n', ' '); } } if (Message.empty()) // either !SytheticMessage, or we failed to make one. Info.FormatDiagnostic(Message); LastDiag->Fixes.push_back(Fix{Message.str(), std::move(Edits)}); return true; }; if (!isNote(DiagLevel)) { // Handle the new main diagnostic. flushLastDiag(); if (Adjuster) { DiagLevel = Adjuster(DiagLevel, Info); if (DiagLevel == DiagnosticsEngine::Ignored) { LastPrimaryDiagnosticWasSuppressed = true; return; } } LastPrimaryDiagnosticWasSuppressed = false; LastDiag = Diag(); LastDiag->ID = Info.getID(); FillDiagBase(*LastDiag); LastDiagWasAdjusted = false; if (!InsideMainFile) LastDiagWasAdjusted = adjustDiagFromHeader(*LastDiag, Info, *LangOpts); if (!Info.getFixItHints().empty()) AddFix(true /* try to invent a message instead of repeating the diag */); if (Fixer) { auto ExtraFixes = Fixer(DiagLevel, Info); LastDiag->Fixes.insert(LastDiag->Fixes.end(), ExtraFixes.begin(), ExtraFixes.end()); } } else { // Handle a note to an existing diagnostic. // If a diagnostic was suppressed due to the suppression filter, // also suppress notes associated with it. if (LastPrimaryDiagnosticWasSuppressed) { return; } if (!LastDiag) { assert(false && "Adding a note without main diagnostic"); IgnoreDiagnostics::log(DiagLevel, Info); return; } if (!Info.getFixItHints().empty()) { // A clang note with fix-it is not a separate diagnostic in clangd. We // attach it as a Fix to the main diagnostic instead. if (!AddFix(false /* use the note as the message */)) IgnoreDiagnostics::log(DiagLevel, Info); } else { // A clang note without fix-its corresponds to clangd::Note. Note N; FillDiagBase(N); LastDiag->Notes.push_back(std::move(N)); } } } void StoreDiags::flushLastDiag() { if (!LastDiag) return; if (mentionsMainFile(*LastDiag) && (!LastDiagWasAdjusted || // Only report the first diagnostic coming from each particular header. IncludeLinesWithErrors.insert(LastDiag->Range.start.line).second)) { Output.push_back(std::move(*LastDiag)); } else { vlog("Dropped diagnostic: {0}: {1}", LastDiag->File, LastDiag->Message); } LastDiag.reset(); } } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp index c80dafeef68c..2089c89e0664 100644 --- a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp +++ b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp @@ -1,954 +1,963 @@ //===--- 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 namespace clang { namespace clangd { namespace { using ::testing::_; using ::testing::ElementsAre; using ::testing::Field; using ::testing::IsEmpty; using ::testing::Pair; using ::testing::UnorderedElementsAre; ::testing::Matcher WithFix(::testing::Matcher FixMatcher) { return Field(&Diag::Fixes, ElementsAre(FixMatcher)); } ::testing::Matcher WithFix(::testing::Matcher FixMatcher1, ::testing::Matcher FixMatcher2) { return Field(&Diag::Fixes, UnorderedElementsAre(FixMatcher1, FixMatcher2)); } ::testing::Matcher WithNote(::testing::Matcher 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(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; } TEST(DiagnosticsTest, DiagnosticRanges) { // Check we report correct ranges, including various edge-cases. Annotations Test(R"cpp( namespace test{}; void $decl[[foo]](); class T{$explicit[[]]$constructor[[T]](int a);}; 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"); auto TU = TestTU::withCode(Test.code()); TU.ClangTidyChecks = "-*,google-explicit-constructor"; EXPECT_THAT( TU.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\\…' 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'"), // We make sure here that the entire token is highlighted AllOf(Diag(Test.range("constructor"), "single-argument constructors must be marked explicit to " "avoid unintentional implicit conversions"), WithFix(Fix(Test.range("explicit"), "explicit ", "insert 'explicit '"))))); } 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, 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.ClangTidyChecks = "-*, readability-uppercase-literal-suffix, " "hicpp-uppercase-literal-suffix"; // Verify that we filter out the duplicated diagnostic message. EXPECT_THAT( TU.build().getDiagnostics(), UnorderedElementsAre(::testing::AllOf( Diag(Test.range(), "floating point literal has suffix 'f', which is not uppercase"), DiagSource(Diag::ClangTidy)))); Test = Annotations(R"cpp( template void func(T) { float f = [[0.3f]]; } void k() { func(123); func(2.0); } )cpp"); TU.Code = 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(), 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)); } )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, modernize-use-trailing-return-type"; 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"), "", "change '\"assert.h\"' to ''"))), 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'"), AllOf( Diag(Test.range("main"), "use a trailing return type for this function"), DiagSource(Diag::ClangTidy), DiagName("modernize-use-trailing-return-type"), // Verify that we don't have "[check-name]" suffix in the message. WithFix(FixMessage("use a trailing return type for this function"))) )); } TEST(DiagnosticTest, ClangTidySuppressionComment) { Annotations Main(R"cpp( int main() { int i = 3; double d = 8 / i; // NOLINT // NOLINTNEXTLINE double e = 8 / i; double f = [[8]] / i; } )cpp"); TestTU TU = TestTU::withCode(Main.code()); TU.ClangTidyChecks = "bugprone-integer-division"; EXPECT_THAT( TU.build().getDiagnostics(), UnorderedElementsAre(::testing::AllOf( Diag(Main.range(), "result of integer division used in a floating " "point context; possible loss of precision"), DiagSource(Diag::ClangTidy), DiagName("bugprone-integer-division")))); } TEST(DiagnosticTest, ClangTidyWarningAsError) { Annotations Main(R"cpp( int main() { int i = 3; double f = [[8]] / i; } )cpp"); TestTU TU = TestTU::withCode(Main.code()); TU.ClangTidyChecks = "bugprone-integer-division"; TU.ClangTidyWarningsAsErrors = "bugprone-integer-division"; EXPECT_THAT( TU.build().getDiagnostics(), UnorderedElementsAre(::testing::AllOf( Diag(Main.range(), "result of integer division used in a floating " "point context; possible loss of precision"), DiagSource(Diag::ClangTidy), DiagName("bugprone-integer-division"), DiagSeverity(DiagnosticsEngine::Error)))); } TEST(DiagnosticTest, LongFixMessages) { // We limit the size of printed code. Annotations Source(R"cpp( int main() { 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( int main() { int ident; [[ide\ n]] = 10; } )cpp"); TU = TestTU::withCode(Source.code()); EXPECT_THAT(TU.build().getDiagnostics(), ElementsAre(WithFix( Fix(Source.range(), "ident", "change 'ide\\…' to 'ident'")))); } TEST(DiagnosticTest, ClangTidyWarningAsErrorTrumpsSuppressionComment) { Annotations Main(R"cpp( int main() { int i = 3; double f = [[8]] / i; // NOLINT } )cpp"); TestTU TU = TestTU::withCode(Main.code()); TU.ClangTidyChecks = "bugprone-integer-division"; TU.ClangTidyWarningsAsErrors = "bugprone-integer-division"; EXPECT_THAT( TU.build().getDiagnostics(), UnorderedElementsAre(::testing::AllOf( Diag(Main.range(), "result of integer division used in a floating " "point context; possible loss of precision"), DiagSource(Diag::ClangTidy), DiagName("bugprone-integer-division"), DiagSeverity(DiagnosticsEngine::Error)))); } TEST(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>> LSPDiags; toLSPDiags(D, MainFile, Opts, [&](clangd::Diagnostic LSPDiag, ArrayRef Fixes) { LSPDiags.push_back( {std::move(LSPDiag), std::vector(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 Fixes) { LSPDiags.push_back( {std::move(LSPDiag), std::vector(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 buildIndexWithSymbol(llvm::ArrayRef Syms) { SymbolSlab::Builder Slab; for (const auto &S : Syms) { Symbol Sym = cls(S.QName); Sym.Flags |= Symbol::IndexedForCodeCompletion; Sym.CanonicalDeclaration.FileURI = S.DeclaringFile.c_str(); Sym.Definition.FileURI = S.DeclaringFile.c_str(); Sym.IncludeHeaders.emplace_back(S.IncludeHeader, 1); Slab.insert(Sym); } return MemIndex::build(std::move(Slab).build(), RefSlab(), RelationSlab()); } TEST(IncludeFixerTest, IncompleteType) { Annotations Test(R"cpp( $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(), 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( $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(IncludeFixerTest, NoCrashOnTemplateInstantiations) { Annotations Test(R"cpp( template struct Templ { template typename U::type operator=(const U &); }; struct A { Templ s; A() { [[a]]; } // this caused crashes if we compute scopes lazily. }; )cpp"); auto TU = TestTU::withCode(Test.code()); auto Index = buildIndexWithSymbol({}); TU.ExternalIndex = Index.get(); EXPECT_THAT(TU.build().getDiagnostics(), ElementsAre(Diag(Test.range(), "use of undeclared identifier 'a'"))); } TEST(DiagsInHeaders, DiagInsideHeader) { Annotations Main(R"cpp( #include [["a.h"]] void foo() {})cpp"); Annotations Header("[[no_type_spec]];"); 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()}}; 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(IgnoreDiags, FromNonWrittenSources) { Annotations Main(R"cpp( #include [["a.h"]] void foo() {})cpp"); Annotations Header(R"cpp( int x = 5/0; int b = [[FOO]];)cpp"); TestTU TU = TestTU::withCode(Main.code()); TU.AdditionalFiles = {{"a.h", Header.code()}}; TU.ExtraArgs = {"-DFOO=NOOO"}; EXPECT_THAT(TU.build().getDiagnostics(), UnorderedElementsAre()); } +TEST(IgnoreDiags, FromNonWrittenInclude) { + TestTU TU = TestTU::withCode(""); + 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()); +} + } // namespace } // namespace clangd } // namespace clang