11 changes: 11 additions & 0 deletions clang-tools-extra/clangd/Headers.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
#include "Path.h"
#include "Protocol.h"
#include "SourceCode.h"
#include "index/Index.h"
#include "clang/Format/Format.h"
#include "clang/Lex/HeaderSearch.h"
#include "clang/Lex/PPCallbacks.h"
#include "clang/Tooling/Inclusions/HeaderIncludes.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Support/Error.h"
Expand All @@ -37,6 +39,15 @@ struct HeaderFile {
bool valid() const;
};

/// Creates a `HeaderFile` from \p Header which can be either a URI or a literal
/// include.
llvm::Expected<HeaderFile> toHeaderFile(llvm::StringRef Header,
llvm::StringRef HintPath);

// Returns include headers for \p Sym sorted by popularity. If two headers are
// equally popular, prefer the shorter one.
llvm::SmallVector<llvm::StringRef, 1> getRankedIncludes(const Symbol &Sym);

// An #include directive that we found in the main file.
struct Inclusion {
Range R; // Inclusion range.
Expand Down
113 changes: 113 additions & 0 deletions clang-tools-extra/clangd/IncludeFixer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//===--- IncludeFixer.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 "IncludeFixer.h"
#include "AST.h"
#include "Diagnostics.h"
#include "Logger.h"
#include "SourceCode.h"
#include "Trace.h"
#include "index/Index.h"
#include "clang/AST/Decl.h"
#include "clang/AST/Type.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/DiagnosticSema.h"
#include "llvm/ADT/None.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FormatVariadic.h"

namespace clang {
namespace clangd {

std::vector<Fix> IncludeFixer::fix(DiagnosticsEngine::Level DiagLevel,
const clang::Diagnostic &Info) const {
if (IndexRequestCount >= IndexRequestLimit)
return {}; // Avoid querying index too many times in a single parse.
switch (Info.getID()) {
case diag::err_incomplete_type:
case diag::err_incomplete_member_access:
case diag::err_incomplete_base_class:
// Incomplete type diagnostics should have a QualType argument for the
// incomplete type.
for (unsigned Idx = 0; Idx < Info.getNumArgs(); ++Idx) {
if (Info.getArgKind(Idx) == DiagnosticsEngine::ak_qualtype) {
auto QT = QualType::getFromOpaquePtr((void *)Info.getRawArg(Idx));
if (const Type *T = QT.getTypePtrOrNull())
if (T->isIncompleteType())
return fixIncompleteType(*T);
}
}
}
return {};
}

std::vector<Fix> IncludeFixer::fixIncompleteType(const Type &T) const {
// Only handle incomplete TagDecl type.
const TagDecl *TD = T.getAsTagDecl();
if (!TD)
return {};
std::string TypeName = printQualifiedName(*TD);
trace::Span Tracer("Fix include for incomplete type");
SPAN_ATTACH(Tracer, "type", TypeName);
vlog("Trying to fix include for incomplete type {0}", TypeName);

auto ID = getSymbolID(TD);
if (!ID)
return {};
++IndexRequestCount;
// FIXME: consider batching the requests for all diagnostics.
// FIXME: consider caching the lookup results.
LookupRequest Req;
Req.IDs.insert(*ID);
llvm::Optional<Symbol> Matched;
Index.lookup(Req, [&](const Symbol &Sym) {
if (Matched)
return;
Matched = Sym;
});

if (!Matched || Matched->IncludeHeaders.empty() || !Matched->Definition ||
Matched->CanonicalDeclaration.FileURI != Matched->Definition.FileURI)
return {};
return fixesForSymbol(*Matched);
}

std::vector<Fix> IncludeFixer::fixesForSymbol(const Symbol &Sym) const {
auto Inserted = [&](llvm::StringRef Header)
-> llvm::Expected<std::pair<std::string, bool>> {
auto ResolvedDeclaring =
toHeaderFile(Sym.CanonicalDeclaration.FileURI, File);
if (!ResolvedDeclaring)
return ResolvedDeclaring.takeError();
auto ResolvedInserted = toHeaderFile(Header, File);
if (!ResolvedInserted)
return ResolvedInserted.takeError();
return std::make_pair(
Inserter->calculateIncludePath(*ResolvedDeclaring, *ResolvedInserted),
Inserter->shouldInsertInclude(*ResolvedDeclaring, *ResolvedInserted));
};

std::vector<Fix> Fixes;
for (const auto &Inc : getRankedIncludes(Sym)) {
if (auto ToInclude = Inserted(Inc)) {
if (ToInclude->second)
if (auto Edit = Inserter->insert(ToInclude->first))
Fixes.push_back(
Fix{llvm::formatv("Add include {0} for symbol {1}{2}",
ToInclude->first, Sym.Scope, Sym.Name),
{std::move(*Edit)}});
} else {
vlog("Failed to calculate include insertion for {0} into {1}: {2}", File,
Inc, ToInclude.takeError());
}
}
return Fixes;
}

} // namespace clangd
} // namespace clang
54 changes: 54 additions & 0 deletions clang-tools-extra/clangd/IncludeFixer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//===--- IncludeFixer.h ------------------------------------------*- 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
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_INCLUDE_FIXER_H
#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_INCLUDE_FIXER_H

#include "Diagnostics.h"
#include "Headers.h"
#include "index/Index.h"
#include "clang/AST/Type.h"
#include "clang/Basic/Diagnostic.h"
#include "llvm/ADT/StringRef.h"
#include <memory>

namespace clang {
namespace clangd {

/// Attempts to recover from error diagnostics by suggesting include insertion
/// fixes. For example, member access into incomplete type can be fixes by
/// include headers with the definition.
class IncludeFixer {
public:
IncludeFixer(llvm::StringRef File, std::shared_ptr<IncludeInserter> Inserter,
const SymbolIndex &Index, unsigned IndexRequestLimit)
: File(File), Inserter(std::move(Inserter)), Index(Index),
IndexRequestLimit(IndexRequestLimit) {}

/// Returns include insertions that can potentially recover the diagnostic.
std::vector<Fix> fix(DiagnosticsEngine::Level DiagLevel,
const clang::Diagnostic &Info) const;

private:
/// Attempts to recover diagnostic caused by an incomplete type \p T.
std::vector<Fix> fixIncompleteType(const Type &T) const;

/// Generates header insertion fixes for \p Sym.
std::vector<Fix> fixesForSymbol(const Symbol &Sym) const;

std::string File;
std::shared_ptr<IncludeInserter> Inserter;
const SymbolIndex &Index;
const unsigned IndexRequestLimit; // Make at most 5 index requests.
mutable unsigned IndexRequestCount = 0;
};

} // namespace clangd
} // namespace clang

#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_INCLUDE_FIXER_H
13 changes: 13 additions & 0 deletions clang-tools-extra/clangd/SourceCode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,5 +248,18 @@ llvm::Optional<FileDigest> digestFile(const SourceManager &SM, FileID FID) {
return digest(Content);
}

format::FormatStyle getFormatStyleForFile(llvm::StringRef File,
llvm::StringRef Content,
llvm::vfs::FileSystem *FS) {
auto Style = format::getStyle(format::DefaultFormatStyle, File,
format::DefaultFallbackStyle, Content, FS);
if (!Style) {
log("getStyle() failed for file {0}: {1}. Fallback is LLVM style.", File,
Style.takeError());
Style = format::getLLVMStyle();
}
return *Style;
}

} // namespace clangd
} // namespace clang
7 changes: 7 additions & 0 deletions clang-tools-extra/clangd/SourceCode.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Format/Format.h"
#include "clang/Tooling/Core/Replacement.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/SHA1.h"

namespace clang {
Expand Down Expand Up @@ -91,6 +93,11 @@ llvm::Optional<std::string> getCanonicalPath(const FileEntry *F,
const SourceManager &SourceMgr);

bool isRangeConsecutive(const Range &Left, const Range &Right);

format::FormatStyle getFormatStyleForFile(llvm::StringRef File,
llvm::StringRef Content,
llvm::vfs::FileSystem *FS);

} // namespace clangd
} // namespace clang
#endif
7 changes: 7 additions & 0 deletions clang-tools-extra/clangd/tool/ClangdMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ static llvm::cl::opt<std::string> ClangTidyChecks(
".clang-tidy files)"),
llvm::cl::init(""), llvm::cl::Hidden);

static llvm::cl::opt<bool> SuggestMissingIncludes(
"suggest-missing-includes",
llvm::cl::desc("Attempts to fix diagnostic errors caused by missing "
"includes using index."),
llvm::cl::init(false));

namespace {

/// \brief Supports a test URI scheme with relaxed constraints for lit tests.
Expand Down Expand Up @@ -442,6 +448,7 @@ int main(int argc, char *argv[]) {
/* Default */ tidy::ClangTidyOptions::getDefaults(),
/* Override */ OverrideClangTidyOptions, FSProvider.getFileSystem());
Opts.ClangTidyOptProvider = &ClangTidyOptProvider;
Opts.SuggestMissingIncludes = SuggestMissingIncludes;
ClangdLSPServer LSPServer(
*TransportLayer, FSProvider, CCOpts, CompileCommandsDirPath,
/*UseDirBasedCDB=*/CompileArgsFrom == FilesystemCompileArgs, Opts);
Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/unittests/clangd/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ add_extra_unittest(ClangdTests
CodeCompletionStringsTests.cpp
ContextTests.cpp
DexTests.cpp
DiagnosticsTests.cpp
DraftStoreTests.cpp
ExpectedTypeTest.cpp
FileDistanceTests.cpp
Expand Down
257 changes: 0 additions & 257 deletions clang-tools-extra/unittests/clangd/ClangdUnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,263 +19,6 @@ 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.
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 [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(
Expand Down
48 changes: 1 addition & 47 deletions clang-tools-extra/unittests/clangd/CodeCompleteTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "SourceCode.h"
#include "SyncAPI.h"
#include "TestFS.h"
#include "TestIndex.h"
#include "index/MemIndex.h"
#include "clang/Sema/CodeCompleteConsumer.h"
#include "llvm/Support/Error.h"
Expand Down Expand Up @@ -137,53 +138,6 @@ CodeCompleteResult completions(llvm::StringRef Text,
FilePath);
}

std::string replace(llvm::StringRef Haystack, llvm::StringRef Needle,
llvm::StringRef Repl) {
std::string Result;
llvm::raw_string_ostream OS(Result);
std::pair<llvm::StringRef, llvm::StringRef> Split;
for (Split = Haystack.split(Needle); !Split.second.empty();
Split = Split.first.split(Needle))
OS << Split.first << Repl;
Result += Split.first;
OS.flush();
return Result;
}

// Helpers to produce fake index symbols for memIndex() or completions().
// USRFormat is a regex replacement string for the unqualified part of the USR.
Symbol sym(llvm::StringRef QName, index::SymbolKind Kind,
llvm::StringRef USRFormat) {
Symbol Sym;
std::string USR = "c:"; // We synthesize a few simple cases of USRs by hand!
size_t Pos = QName.rfind("::");
if (Pos == llvm::StringRef::npos) {
Sym.Name = QName;
Sym.Scope = "";
} else {
Sym.Name = QName.substr(Pos + 2);
Sym.Scope = QName.substr(0, Pos + 2);
USR += "@N@" + replace(QName.substr(0, Pos), "::", "@N@"); // ns:: -> @N@ns
}
USR += llvm::Regex("^.*$").sub(USRFormat, Sym.Name); // e.g. func -> @F@func#
Sym.ID = SymbolID(USR);
Sym.SymInfo.Kind = Kind;
Sym.Flags |= Symbol::IndexedForCodeCompletion;
Sym.Origin = SymbolOrigin::Static;
return Sym;
}
Symbol func(llvm::StringRef Name) { // Assumes the function has no args.
return sym(Name, index::SymbolKind::Function, "@F@\\0#"); // no args
}
Symbol cls(llvm::StringRef Name) {
return sym(Name, index::SymbolKind::Class, "@S@\\0");
}
Symbol var(llvm::StringRef Name) {
return sym(Name, index::SymbolKind::Variable, "@\\0");
}
Symbol ns(llvm::StringRef Name) {
return sym(Name, index::SymbolKind::Namespace, "@N@\\0");
}
Symbol withReferences(int N, Symbol S) {
S.References = N;
return S;
Expand Down
351 changes: 351 additions & 0 deletions clang-tools-extra/unittests/clangd/DiagnosticsTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
//===--- 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 "SourceCode.h"
#include "TestIndex.h"
#include "TestTU.h"
#include "index/MemIndex.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.
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 [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(IncludeFixerTest, IncompleteType) {
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.h";
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(
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'")));
}

} // namespace
} // namespace clangd
} // namespace clang

2 changes: 1 addition & 1 deletion clang-tools-extra/unittests/clangd/FileIndexTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ TEST(FileIndexTest, ReferencesInMainFileWithPreamble) {
ParsedAST::build(createInvocationFromCommandLine(Cmd), PreambleData,
llvm::MemoryBuffer::getMemBufferCopy(Main.code()),
std::make_shared<PCHContainerOperations>(), PI.FS,
tidy::ClangTidyOptions::getDefaults());
/*Index=*/nullptr, ParseOptions());
ASSERT_TRUE(AST);
FileIndex Index;
Index.updateMain(MainFile, *AST);
Expand Down
10 changes: 6 additions & 4 deletions clang-tools-extra/unittests/clangd/TUSchedulerTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ using ::testing::AllOf;
using ::testing::AnyOf;
using ::testing::Each;
using ::testing::ElementsAre;
using ::testing::Pair;
using ::testing::Pointee;
using ::testing::UnorderedElementsAre;

Expand All @@ -37,9 +36,12 @@ MATCHER_P2(TUState, State, ActionName, "") {
class TUSchedulerTests : public ::testing::Test {
protected:
ParseInputs getInputs(PathRef File, std::string Contents) {
return ParseInputs{*CDB.getCompileCommand(File),
buildTestFS(Files, Timestamps), std::move(Contents),
tidy::ClangTidyOptions::getDefaults()};
ParseInputs Inputs;
Inputs.CompileCommand = *CDB.getCompileCommand(File);
Inputs.FS = buildTestFS(Files, Timestamps);
Inputs.Contents = std::move(Contents);
Inputs.Opts = ParseOptions();
return Inputs;
}

void updateWithCallback(TUScheduler &S, PathRef File,
Expand Down
54 changes: 54 additions & 0 deletions clang-tools-extra/unittests/clangd/TestIndex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
//===----------------------------------------------------------------------===//

#include "TestIndex.h"
#include "clang/Index/IndexSymbol.h"
#include "llvm/Support/Regex.h"

namespace clang {
namespace clangd {
Expand All @@ -25,6 +27,58 @@ Symbol symbol(llvm::StringRef QName) {
return Sym;
}

static std::string replace(llvm::StringRef Haystack, llvm::StringRef Needle,
llvm::StringRef Repl) {
std::string Result;
llvm::raw_string_ostream OS(Result);
std::pair<llvm::StringRef, llvm::StringRef> Split;
for (Split = Haystack.split(Needle); !Split.second.empty();
Split = Split.first.split(Needle))
OS << Split.first << Repl;
Result += Split.first;
OS.flush();
return Result;
}

// Helpers to produce fake index symbols for memIndex() or completions().
// USRFormat is a regex replacement string for the unqualified part of the USR.
Symbol sym(llvm::StringRef QName, index::SymbolKind Kind,
llvm::StringRef USRFormat) {
Symbol Sym;
std::string USR = "c:"; // We synthesize a few simple cases of USRs by hand!
size_t Pos = QName.rfind("::");
if (Pos == llvm::StringRef::npos) {
Sym.Name = QName;
Sym.Scope = "";
} else {
Sym.Name = QName.substr(Pos + 2);
Sym.Scope = QName.substr(0, Pos + 2);
USR += "@N@" + replace(QName.substr(0, Pos), "::", "@N@"); // ns:: -> @N@ns
}
USR += llvm::Regex("^.*$").sub(USRFormat, Sym.Name); // e.g. func -> @F@func#
Sym.ID = SymbolID(USR);
Sym.SymInfo.Kind = Kind;
Sym.Flags |= Symbol::IndexedForCodeCompletion;
Sym.Origin = SymbolOrigin::Static;
return Sym;
}

Symbol func(llvm::StringRef Name) { // Assumes the function has no args.
return sym(Name, index::SymbolKind::Function, "@F@\\0#"); // no args
}

Symbol cls(llvm::StringRef Name) {
return sym(Name, index::SymbolKind::Class, "@S@\\0");
}

Symbol var(llvm::StringRef Name) {
return sym(Name, index::SymbolKind::Variable, "@\\0");
}

Symbol ns(llvm::StringRef Name) {
return sym(Name, index::SymbolKind::Namespace, "@N@\\0");
}

SymbolSlab generateSymbols(std::vector<std::string> QualifiedNames) {
SymbolSlab::Builder Slab;
for (llvm::StringRef QName : QualifiedNames)
Expand Down
13 changes: 13 additions & 0 deletions clang-tools-extra/unittests/clangd/TestIndex.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ namespace clangd {
// Creates Symbol instance and sets SymbolID to given QualifiedName.
Symbol symbol(llvm::StringRef QName);

// Helpers to produce fake index symbols with proper SymbolID.
// USRFormat is a regex replacement string for the unqualified part of the USR.
Symbol sym(llvm::StringRef QName, index::SymbolKind Kind,
llvm::StringRef USRFormat);
// Creats a function symbol assuming no function arg.
Symbol func(llvm::StringRef Name);
// Creates a class symbol.
Symbol cls(llvm::StringRef Name);
// Creates a variable symbol.
Symbol var(llvm::StringRef Name);
// Creates a namespace symbol.
Symbol ns(llvm::StringRef Name);

// Create a slab of symbols with the given qualified names as IDs and names.
SymbolSlab generateSymbols(std::vector<std::string> QualifiedNames);

Expand Down
7 changes: 5 additions & 2 deletions clang-tools-extra/unittests/clangd/TestTU.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ ParsedAST TestTU::build() const {
Inputs.CompileCommand.Directory = testRoot();
Inputs.Contents = Code;
Inputs.FS = buildTestFS({{FullFilename, Code}, {FullHeaderName, HeaderCode}});
Inputs.ClangTidyOpts = tidy::ClangTidyOptions::getDefaults();
Inputs.ClangTidyOpts.Checks = ClangTidyChecks;
Inputs.Opts = ParseOptions();
Inputs.Opts.ClangTidyOpts.Checks = ClangTidyChecks;
Inputs.Index = ExternalIndex;
if (Inputs.Index)
Inputs.Opts.SuggestMissingIncludes = true;
auto PCHs = std::make_shared<PCHContainerOperations>();
auto CI = buildCompilerInvocation(Inputs);
assert(CI && "Failed to build compilation invocation.");
Expand Down
2 changes: 2 additions & 0 deletions clang-tools-extra/unittests/clangd/TestTU.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ struct TestTU {
std::vector<const char *> ExtraArgs;

llvm::Optional<std::string> ClangTidyChecks;
// Index to use when building AST.
const SymbolIndex *ExternalIndex = nullptr;

ParsedAST build() const;
SymbolSlab headerSymbols() const;
Expand Down