ClangCodeModel: Support access type categorization

... with "Find Usages", as we do in the built-in code model.
Note 1: This is very slow, so it's for now only enabled if the
        search results come from a small number of files.
        Possible ways of speeding up the operation
        to be investigated.
Note 2: All test cases from the old code model also pass here,
        but checking with non-trivial real-world projects
        shows a lot of mis-categorizations.
        Well will fix them one by one.
Note 3: This functionality requires clangd >= 13.

Change-Id: Ib3500b52996dbbf9d7d9712d729179bcbd3262fc
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
Christian Kandeler
2021-04-21 14:29:49 +02:00
parent 11f574fef9
commit 6dc3c1f156
19 changed files with 1042 additions and 29 deletions

View File

@@ -1,7 +1,7 @@
add_qtc_plugin(ClangCodeModel
CONDITION TARGET libclang
DEPENDS ClangSupport CPlusPlus
PLUGIN_DEPENDS Core CppTools LanguageClient TextEditor
PLUGIN_DEPENDS Core CppTools LanguageClient QtSupport TextEditor
PLUGIN_TEST_DEPENDS CppEditor QmakeProjectManager
SOURCES
clangactivationsequencecontextprocessor.cpp clangactivationsequencecontextprocessor.h
@@ -52,5 +52,6 @@ extend_qtc_plugin(ClangCodeModel
test/clangautomationutils.cpp test/clangautomationutils.h
test/clangbatchfileprocessor.cpp test/clangbatchfileprocessor.h
test/clangcodecompletion_test.cpp test/clangcodecompletion_test.h
test/clangdtests.cpp test/clangdtests.h
test/data/clangtestdata.qrc
)

View File

@@ -96,11 +96,13 @@ equals(TEST, 1) {
test/clangautomationutils.h \
test/clangbatchfileprocessor.h \
test/clangcodecompletion_test.h \
test/clangdtests.h
SOURCES += \
test/clangautomationutils.cpp \
test/clangbatchfileprocessor.cpp \
test/clangcodecompletion_test.cpp \
test/clangdtests.cpp
RESOURCES += test/data/clangtestdata.qrc
OTHER_FILES += $$files(test/data/*)

View File

@@ -8,6 +8,7 @@ QtcPlugin {
Depends { name: "Core" }
Depends { name: "CppTools" }
Depends { name: "ProjectExplorer" }
Depends { name: "QtSupport"; condition: qtc.testsEnabled }
Depends { name: "TextEditor" }
Depends { name: "Utils" }
Depends { name: "ClangSupport" }
@@ -114,6 +115,8 @@ QtcPlugin {
"clangbatchfileprocessor.h",
"clangcodecompletion_test.cpp",
"clangcodecompletion_test.h",
"clangdtests.cpp",
"clangdtests.h",
"data/clangtestdata.qrc",
]
}

View File

@@ -10,3 +10,5 @@ QTC_PLUGIN_DEPENDS += \
QTC_TEST_DEPENDS += \
cppeditor \
qmakeprojectmanager
equals(TEST, 1): QTC_PLUGIN_DEPENDS += qtsupport

View File

@@ -32,6 +32,7 @@
#ifdef WITH_TESTS
# include "test/clangbatchfileprocessor.h"
# include "test/clangcodecompletion_test.h"
# include "test/clangdtests.h"
#endif
#include <coreplugin/actionmanager/actioncontainer.h>
@@ -204,6 +205,7 @@ QVector<QObject *> ClangCodeModelPlugin::createTestObjects() const
{
return {
new Tests::ClangCodeCompletionTest,
new Tests::ClangdTests,
};
}
#endif

View File

@@ -25,10 +25,21 @@
#include "clangdclient.h"
#include <coreplugin/find/searchresultitem.h>
#include <coreplugin/find/searchresultwindow.h>
#include <cplusplus/FindUsages.h>
#include <cpptools/cppcodemodelsettings.h>
#include <cpptools/cppfindreferences.h>
#include <cpptools/cpptoolsreuse.h>
#include <languageclient/languageclientinterface.h>
#include <QFile>
#include <QHash>
#include <QPointer>
#include <QRegularExpression>
using namespace CPlusPlus;
using namespace Core;
using namespace LanguageClient;
using namespace LanguageServerProtocol;
@@ -36,13 +47,240 @@ namespace ClangCodeModel {
namespace Internal {
static Q_LOGGING_CATEGORY(clangdLog, "qtc.clangcodemodel.clangd", QtWarningMsg);
static QString indexingToken() { return "backgroundIndexProgress"; }
class AstParams : public JsonObject
{
public:
AstParams() {}
AstParams(const TextDocumentIdentifier &document, const Range &range);
using JsonObject::JsonObject;
// The open file to inspect.
TextDocumentIdentifier textDocument() const
{ return typedValue<TextDocumentIdentifier>(textDocumentKey); }
void setTextDocument(const TextDocumentIdentifier &id) { insert(textDocumentKey, id); }
// The region of the source code whose AST is fetched. The highest-level node that entirely
// contains the range is returned.
Utils::optional<Range> range() const { return optionalValue<Range>(rangeKey); }
void setRange(const Range &range) { insert(rangeKey, range); }
bool isValid() const override { return contains(textDocumentKey); }
};
class AstNode : public JsonObject
{
public:
using JsonObject::JsonObject;
static constexpr char roleKey[] = "role";
static constexpr char arcanaKey[] = "arcana";
// The general kind of node, such as “expression”. Corresponds to clangs base AST node type,
// such as Expr. The most common are “expression”, “statement”, “type” and “declaration”.
QString role() const { return typedValue<QString>(roleKey); }
// The specific kind of node, such as “BinaryOperator”. Corresponds to clangs concrete
// node class, with Expr etc suffix dropped.
QString kind() const { return typedValue<QString>(kindKey); }
// Brief additional details, such as ||. Information present here depends on the node kind.
Utils::optional<QString> detail() const { return optionalValue<QString>(detailKey); }
// One line dump of information, similar to that printed by clang -Xclang -ast-dump.
// Only available for certain types of nodes.
Utils::optional<QString> arcana() const { return optionalValue<QString>(arcanaKey); }
// The part of the code that produced this node. Missing for implicit nodes, nodes produced
// by macro expansion, etc.
Range range() const { return typedValue<Range>(rangeKey); }
// Descendants describing the internal structure. The tree of nodes is similar to that printed
// by clang -Xclang -ast-dump, or that traversed by clang::RecursiveASTVisitor.
Utils::optional<QList<AstNode>> children() const { return optionalArray<AstNode>(childrenKey); }
bool hasRange() const { return contains(rangeKey); }
bool arcanaContains(const QString &s) const
{
const Utils::optional<QString> arcanaString = arcana();
return arcanaString && arcanaString->contains(s);
}
bool detailIs(const QString &s) const
{
return detail() && detail().value() == s;
}
QString type() const
{
const Utils::optional<QString> arcanaString = arcana();
if (!arcanaString)
return {};
const int quote1Offset = arcanaString->indexOf('\'');
if (quote1Offset == -1)
return {};
const int quote2Offset = arcanaString->indexOf('\'', quote1Offset + 1);
if (quote2Offset == -1)
return {};
return arcanaString->mid(quote1Offset + 1, quote2Offset - quote1Offset - 1);
}
// Returns true <=> the type is "recursively const".
// E.g. returns true for "const int &", "const int *" and "const int * const *",
// and false for "int &" and "const int **".
// For non-pointer types such as "int", we check whether they are uses as lvalues
// or rvalues.
bool hasConstType() const
{
QString theType = type();
if (theType.endsWith("const"))
theType.chop(5);
const int ptrRefCount = theType.count('*') + theType.count('&');
const int constCount = theType.count("const");
if (ptrRefCount == 0)
return constCount > 0 || detailIs("LValueToRValue");
return ptrRefCount <= constCount;
}
bool childContainsRange(int index, const Range &range) const
{
const Utils::optional<QList<AstNode>> childList = children();
return childList && childList->size() > index
&& childList->at(index).range().contains(range);
}
QString operatorString() const
{
if (kind() == "BinaryOperator")
return detail().value_or(QString());
QTC_ASSERT(kind() == "CXXOperatorCall", return {});
const Utils::optional<QString> arcanaString = arcana();
if (!arcanaString)
return {};
const int closingQuoteOffset = arcanaString->lastIndexOf('\'');
if (closingQuoteOffset <= 0)
return {};
const int openingQuoteOffset = arcanaString->lastIndexOf('\'', closingQuoteOffset - 1);
if (openingQuoteOffset == -1)
return {};
return arcanaString->mid(openingQuoteOffset + 1, closingQuoteOffset
- openingQuoteOffset - 1);
}
bool isValid() const override
{
return contains(roleKey) && contains(kindKey);
}
};
static QList<AstNode> getAstPath(const AstNode &root, const Range &range)
{
QList<AstNode> path;
QList<AstNode> queue{root};
bool isRoot = true;
while (!queue.isEmpty()) {
AstNode curNode = queue.takeFirst();
if (!isRoot && !curNode.hasRange())
continue;
if (curNode.range() == range)
return path << curNode;
if (isRoot || curNode.range().contains(range)) {
path << curNode;
const auto children = curNode.children();
if (!children)
break;
queue = children.value();
}
isRoot = false;
}
return path;
}
static Usage::Type getUsageType(const QList<AstNode> &path)
{
bool potentialWrite = false;
const bool symbolIsDataType = path.last().role() == "type" && path.last().kind() == "Record";
for (auto pathIt = path.rbegin(); pathIt != path.rend(); ++pathIt) {
if (pathIt->arcanaContains("non_odr_use_unevaluated"))
return Usage::Type::Other;
if (pathIt->kind() == "CXXDelete")
return Usage::Type::Write;
if (pathIt->kind() == "CXXNew")
return Usage::Type::Other;
if (pathIt->kind() == "Switch" || pathIt->kind() == "If")
return Usage::Type::Read;
if (pathIt->kind() == "Call" || pathIt->kind() == "CXXMemberCall")
return potentialWrite ? Usage::Type::WritableRef : Usage::Type::Read;
if ((pathIt->kind() == "DeclRef" || pathIt->kind() == "Member")
&& pathIt->arcanaContains("lvalue")) {
potentialWrite = true;
}
if (pathIt->role() == "declaration") {
if (symbolIsDataType)
return Usage::Type::Other;
if (pathIt->arcanaContains("cinit")) {
if (pathIt == path.rbegin())
return Usage::Type::Initialization;
if (pathIt->childContainsRange(0, path.last().range()))
return Usage::Type::Initialization;
if (!pathIt->hasConstType())
return Usage::Type::WritableRef;
return Usage::Type::Read;
}
return Usage::Type::Declaration;
}
if (pathIt->kind() == "MemberInitializer")
return pathIt == path.rbegin() ? Usage::Type::Write : Usage::Type::Read;
if (pathIt->kind() == "UnaryOperator"
&& (pathIt->detailIs("++") || pathIt->detailIs("--"))) {
return Usage::Type::Write;
}
// LLVM uses BinaryOperator only for built-in types; for classes, CXXOperatorCall
// is used. The latter has an additional node at index 0, so the left-hand side
// of an assignment is at index 1.
const bool isBinaryOp = pathIt->kind() == "BinaryOperator";
const bool isOpCall = pathIt->kind() == "CXXOperatorCall";
if (isBinaryOp || isOpCall) {
if (isOpCall && symbolIsDataType) // Constructor invocation.
return Usage::Type::Other;
const QString op = pathIt->operatorString();
if (op.endsWith("=") && op != "==") { // Assignment.
const int lhsIndex = isBinaryOp ? 0 : 1;
if (pathIt->childContainsRange(lhsIndex, path.last().range()))
return Usage::Type::Write;
return potentialWrite ? Usage::Type::WritableRef : Usage::Type::Read;
}
return Usage::Type::Read;
}
if (pathIt->kind() == "ImplicitCast") {
if (pathIt->detailIs("FunctionToPointerDecay"))
return Usage::Type::Other;
if (pathIt->hasConstType())
return Usage::Type::Read;
potentialWrite = true;
continue;
}
}
return Usage::Type::Other;
}
class AstRequest : public Request<AstNode, std::nullptr_t, AstParams>
{
public:
using Request::Request;
explicit AstRequest(const AstParams &params) : Request("textDocument/ast", params) {}
};
static BaseClientInterface *clientInterface(const Utils::FilePath &jsonDbDir)
{
Utils::CommandLine cmd{CppTools::codeModelSettings()->clangdFilePath(),
{"--index", "--background-index", "--limit-results=0"}};
{"--background-index", "--limit-results=0"}};
if (!jsonDbDir.isEmpty())
cmd.addArg("--compile-commands-dir=" + jsonDbDir.toString());
if (clangdLog().isDebugEnabled())
@@ -52,8 +290,44 @@ static BaseClientInterface *clientInterface(const Utils::FilePath &jsonDbDir)
return interface;
}
class ReferencesFileData {
public:
QList<QPair<Range, QString>> rangesAndLineText;
QString fileContent;
AstNode ast;
};
class ReferencesData {
public:
void setCanceled() { search->setUserData(true); }
bool isCanceled() const { return search && search->userData().toBool(); }
QMap<DocumentUri, ReferencesFileData> fileData;
QList<MessageId> pendingAstRequests;
QPointer<SearchResult> search;
quint64 key;
};
class ClangdClient::Private
{
public:
Private(ClangdClient *q) : q(q) {}
void handleFindUsagesResult(quint64 key, const QList<Location> &locations);
void addSearchResultsForFile(const ReferencesData &refData, const Utils::FilePath &file,
const ReferencesFileData &fileData);
void reportAllSearchResultsAndFinish(const ReferencesData &data);
void finishSearch(const ReferencesData &refData, bool canceled);
ClangdClient * const q;
QHash<quint64, ReferencesData> runningFindUsages;
Utils::optional<QVersionNumber> versionNumber;
quint64 nextFindUsagesKey = 0;
bool isFullyIndexed = false;
bool isTesting = false;
};
ClangdClient::ClangdClient(ProjectExplorer::Project *project, const Utils::FilePath &jsonDbDir)
: Client(clientInterface(jsonDbDir))
: Client(clientInterface(jsonDbDir)), d(new Private(this))
{
setName(tr("clangd"));
LanguageFilter langFilter;
@@ -69,11 +343,252 @@ ClangdClient::ClangdClient(ProjectExplorer::Project *project, const Utils::FileP
setCurrentProject(project);
connect(this, &Client::workDone, this, [this](const ProgressToken &token) {
const QString * const val = Utils::get_if<QString>(&token);
if (val && *val == indexingToken())
m_isFullyIndexed = true;
if (val && *val == indexingToken()) {
d->isFullyIndexed = true;
emit indexingFinished();
}
});
connect(this, &Client::initialized, this, [this] {
// If we get this signal while there are pending searches, it means that
// the client was re-initialized, i.e. clangd crashed.
// Report all search results found so far.
for (quint64 key : d->runningFindUsages.keys())
d->reportAllSearchResultsAndFinish(d->runningFindUsages.value(key));
QTC_CHECK(d->runningFindUsages.isEmpty());
});
start();
}
ClangdClient::~ClangdClient()
{
delete d;
}
bool ClangdClient::isFullyIndexed() const { return d->isFullyIndexed; }
void ClangdClient::openExtraFile(const Utils::FilePath &filePath, const QString &content)
{
QFile cxxFile(filePath.toString());
if (content.isEmpty() && !cxxFile.open(QIODevice::ReadOnly))
return;
TextDocumentItem item;
item.setLanguageId("cpp");
item.setUri(DocumentUri::fromFilePath(filePath));
item.setText(!content.isEmpty() ? content : QString::fromUtf8(cxxFile.readAll()));
item.setVersion(0);
sendContent(DidOpenTextDocumentNotification(DidOpenTextDocumentParams(item)));
}
void ClangdClient::closeExtraFile(const Utils::FilePath &filePath)
{
sendContent(DidCloseTextDocumentNotification(DidCloseTextDocumentParams(
TextDocumentIdentifier{DocumentUri::fromFilePath(filePath)})));
}
void ClangdClient::findUsages(TextEditor::TextDocument *document, const QTextCursor &cursor)
{
if (versionNumber() < QVersionNumber(13)) {
symbolSupport().findUsages(document, cursor);
return;
}
QTextCursor termCursor(cursor);
termCursor.select(QTextCursor::WordUnderCursor);
const QString searchTerm = termCursor.selectedText(); // TODO: This will be wrong for e.g. operators. Use a Symbol info request to get the real symbol string.
if (searchTerm.isEmpty())
return;
ReferencesData refData;
refData.search = SearchResultWindow::instance()->startNewSearch(
tr("C++ Usages:"),
{},
searchTerm,
SearchResultWindow::SearchOnly,
SearchResultWindow::PreserveCaseDisabled,
"CppEditor");
refData.search->setFilter(new CppTools::CppSearchResultFilter);
connect(refData.search, &SearchResult::activated, [](const SearchResultItem& item) {
Core::EditorManager::openEditorAtSearchResult(item);
});
SearchResultWindow::instance()->popup(IOutputPane::ModeSwitch | IOutputPane::WithFocus);
refData.key = d->nextFindUsagesKey++;
d->runningFindUsages.insert(refData.key, refData);
const Utils::optional<MessageId> requestId = symbolSupport().findUsages(
document, cursor, [this, key = refData.key](const QList<Location> &locations) {
d->handleFindUsagesResult(key, locations);
});
if (!requestId) {
d->finishSearch(refData, false);
return;
}
connect(refData.search, &SearchResult::cancelled, this, [this, requestId, key = refData.key] {
const auto refData = d->runningFindUsages.find(key);
if (refData == d->runningFindUsages.end())
return;
cancelRequest(*requestId);
refData->setCanceled();
refData->search->disconnect(this);
d->finishSearch(*refData, true);
});
}
void ClangdClient::enableTesting() { d->isTesting = true; }
QVersionNumber ClangdClient::versionNumber() const
{
if (d->versionNumber)
return d->versionNumber.value();
const QRegularExpression versionPattern("^clangd version (\\d+)\\.(\\d+)\\.(\\d+).*$");
QTC_CHECK(versionPattern.isValid());
const QRegularExpressionMatch match = versionPattern.match(serverVersion());
if (match.isValid()) {
d->versionNumber.emplace({match.captured(1).toInt(), match.captured(2).toInt(),
match.captured(3).toInt()});
} else {
qCWarning(clangdLog) << "Failed to parse clangd server string" << serverVersion();
d->versionNumber.emplace({0});
}
return d->versionNumber.value();
}
void ClangdClient::Private::handleFindUsagesResult(quint64 key, const QList<Location> &locations)
{
const auto refData = runningFindUsages.find(key);
if (refData == runningFindUsages.end())
return;
if (!refData->search || refData->isCanceled()) {
finishSearch(*refData, true);
return;
}
refData->search->disconnect(q);
qCDebug(clangdLog) << "found" << locations.size() << "locations";
if (locations.isEmpty()) {
finishSearch(*refData, false);
return;
}
QObject::connect(refData->search, &SearchResult::cancelled, q, [this, key] {
const auto refData = runningFindUsages.find(key);
if (refData == runningFindUsages.end())
return;
refData->setCanceled();
refData->search->disconnect(q);
for (const MessageId &id : qAsConst(refData->pendingAstRequests))
q->cancelRequest(id);
refData->pendingAstRequests.clear();
finishSearch(*refData, true);
});
for (const Location &loc : locations) // TODO: Can contain duplicates. Rather fix in clang than work around it here.
refData->fileData[loc.uri()].rangesAndLineText << qMakePair(loc.range(), QString()); // TODO: Can we assume that locations for the same file are grouped?
for (auto it = refData->fileData.begin(); it != refData->fileData.end(); ++it) {
const QStringList lines = SymbolSupport::getFileContents(
it.key().toFilePath().toString());
it->fileContent = lines.join('\n');
for (auto &rangeWithText : it.value().rangesAndLineText) {
const int lineNo = rangeWithText.first.start().line();
if (lineNo >= 0 && lineNo < lines.size())
rangeWithText.second = lines.at(lineNo);
}
}
qCDebug(clangdLog) << "document count is" << refData->fileData.size();
if (refData->fileData.size() > 15) { // TODO: If we need to keep this, make it configurable.
qCDebug(clangdLog) << "skipping AST retrieval";
reportAllSearchResultsAndFinish(*refData);
return;
}
for (auto it = refData->fileData.begin(); it != refData->fileData.end(); ++it) {
const bool extraOpen = !q->documentForFilePath(it.key().toFilePath());
if (extraOpen)
q->openExtraFile(it.key().toFilePath(), it->fileContent);
it->fileContent.clear();
AstParams params;
params.setTextDocument(TextDocumentIdentifier(it.key()));
AstRequest request(params);
request.setResponseCallback([this, key, loc = it.key(), request]
(AstRequest::Response response) {
qCDebug(clangdLog) << "AST response for" << loc.toFilePath();
const auto refData = runningFindUsages.find(key);
if (refData == runningFindUsages.end())
return;
if (!refData->search || refData->isCanceled())
return;
ReferencesFileData &data = refData->fileData[loc];
const auto result = response.result();
if (result)
data.ast = *result;
refData->pendingAstRequests.removeOne(request.id());
qCDebug(clangdLog) << refData->pendingAstRequests.size()
<< "AST requests still pending";
addSearchResultsForFile(*refData, loc.toFilePath(), data);
refData->fileData.remove(loc);
if (refData->pendingAstRequests.isEmpty()) {
qDebug(clangdLog) << "retrieved all ASTs";
finishSearch(*refData, false);
}
});
qCDebug(clangdLog) << "requesting AST for" << it.key().toFilePath();
refData->pendingAstRequests << request.id();
q->sendContent(request);
if (extraOpen)
q->closeExtraFile(it.key().toFilePath());
}
}
void ClangdClient::Private::addSearchResultsForFile(const ReferencesData &refData,
const Utils::FilePath &file,
const ReferencesFileData &fileData)
{
QList<SearchResultItem> items;
qCDebug(clangdLog) << file << "has valid AST:" << fileData.ast.isValid();
for (const auto &rangeWithText : fileData.rangesAndLineText) {
const Range &range = rangeWithText.first;
const Usage::Type usageType = fileData.ast.isValid()
? getUsageType(getAstPath(fileData.ast, qAsConst(range)))
: Usage::Type::Other;
SearchResultItem item;
item.setUserData(int(usageType));
item.setStyle(CppTools::colorStyleForUsageType(usageType));
item.setFilePath(file);
item.setMainRange(SymbolSupport::convertRange(range));
item.setUseTextEditorFont(true);
item.setLineText(rangeWithText.second);
items << item;
}
if (isTesting)
emit q->foundReferences(items);
else
refData.search->addResults(items, SearchResult::AddOrdered);
}
void ClangdClient::Private::reportAllSearchResultsAndFinish(const ReferencesData &refData)
{
for (auto it = refData.fileData.begin(); it != refData.fileData.end(); ++it)
addSearchResultsForFile(refData, it.key().toFilePath(), it.value());
finishSearch(refData, refData.isCanceled());
}
void ClangdClient::Private::finishSearch(const ReferencesData &refData, bool canceled)
{
if (isTesting) {
emit q->findUsagesDone();
} else if (refData.search) {
refData.search->finishSearch(canceled);
refData.search->disconnect(q);
}
runningFindUsages.remove(refData.key);
}
} // namespace Internal
} // namespace ClangCodeModel

View File

@@ -27,7 +27,11 @@
#include <languageclient/client.h>
#include <QVersionNumber>
namespace Core { class SearchResultItem; }
namespace ProjectExplorer { class Project; }
namespace TextEditor { class TextDocument; }
namespace ClangCodeModel {
namespace Internal {
@@ -37,11 +41,26 @@ class ClangdClient : public LanguageClient::Client
Q_OBJECT
public:
ClangdClient(ProjectExplorer::Project *project, const Utils::FilePath &jsonDbDir);
~ClangdClient() override;
bool isFullyIndexed() const { return m_isFullyIndexed; }
bool isFullyIndexed() const;
QVersionNumber versionNumber() const;
void openExtraFile(const Utils::FilePath &filePath, const QString &content = {});
void closeExtraFile(const Utils::FilePath &filePath);
void findUsages(TextEditor::TextDocument *document, const QTextCursor &cursor);
void enableTesting();
signals:
void indexingFinished();
void foundReferences(const QList<Core::SearchResultItem> &items);
void findUsagesDone();
private:
bool m_isFullyIndexed = false;
class Private;
Private * const d;
};
} // namespace Internal

View File

@@ -64,7 +64,6 @@
#include <utils/runextensions.h>
#include <QApplication>
#include <QFile>
#include <QMenu>
#include <QTextBlock>
#include <QTimer>
@@ -281,7 +280,7 @@ void ClangModelManagerSupport::updateLanguageClient(ProjectExplorer::Project *pr
}
if (Client * const oldClient = clientForProject(project))
LanguageClientManager::shutdownClient(oldClient);
Client * const client = createClient(project, jsonDbDir);
ClangdClient * const client = createClient(project, jsonDbDir);
connect(client, &Client::initialized, this, [client, project, projectInfo, jsonDbDir] {
using namespace ProjectExplorer;
if (!CppTools::codeModelSettings()->useClangd())
@@ -309,18 +308,8 @@ void ClangModelManagerSupport::updateLanguageClient(ProjectExplorer::Project *pr
if (!cxxNode)
return;
QFile cxxFile(cxxNode->filePath().toString());
if (!cxxFile.open(QIODevice::ReadOnly))
return;
using namespace LanguageServerProtocol;
TextDocumentItem item;
item.setLanguageId("text/x-c++src");
item.setUri(DocumentUri::fromFilePath(cxxNode->filePath()));
item.setText(QString::fromUtf8(cxxFile.readAll()));
item.setVersion(0);
client->sendContent(DidOpenTextDocumentNotification(DidOpenTextDocumentParams(item)));
client->sendContent(DidCloseTextDocumentNotification(DidCloseTextDocumentParams(
TextDocumentIdentifier{item.uri()})));
client->openExtraFile(cxxNode->filePath());
client->closeExtraFile(cxxNode->filePath());
});
});
@@ -347,7 +336,9 @@ ClangdClient *ClangModelManagerSupport::clientForProject(
ClangdClient *ClangModelManagerSupport::createClient(ProjectExplorer::Project *project,
const Utils::FilePath &jsonDbDir)
{
return new ClangdClient(project, jsonDbDir);
const auto client = new ClangdClient(project, jsonDbDir);
emit createdClient(client);
return client;
}
void ClangModelManagerSupport::onEditorOpened(Core::IEditor *editor)

View File

@@ -83,6 +83,9 @@ public:
static ClangModelManagerSupport *instance();
signals:
void createdClient(ClangdClient *client);
private:
void onEditorOpened(Core::IEditor *editor);
void onEditorClosed(const QList<Core::IEditor *> &editors);

View File

@@ -97,11 +97,9 @@ void RefactoringEngine::findUsages(const CppTools::CursorInEditor &cursor,
->findUsages(cursor, std::move(callback));
return;
}
// TODO: We want to keep our "access type info" feature.
// Check whether we can support it using clang 12's textDocument/ast request
if (!client->documentOpen(cursor.textDocument()))
client->openDocument(cursor.textDocument()); // TODO: Just a workaround
client->symbolSupport().findUsages(cursor.textDocument(), cursor.cursor());
client->findUsages(cursor.textDocument(), cursor.cursor());
}
} // namespace Internal

View File

@@ -138,5 +138,10 @@ TextEditor::ProposalModelPtr completionResults(TextEditor::BaseTextEditor *textE
return waitForCompletions.proposalModel;
}
QString qrcPath(const QByteArray &relativeFilePath)
{
return QLatin1String(":/unittests/ClangCodeModel/") + QString::fromUtf8(relativeFilePath);
}
} // namespace Internal
} // namespace ClangCodeModel

View File

@@ -39,5 +39,7 @@ TextEditor::ProposalModelPtr completionResults(TextEditor::BaseTextEditor *textE
const QStringList &includePaths = QStringList(),
int timeOutInMs = 10000);
QString qrcPath(const QByteArray &relativeFilePath);
} // namespace Internal
} // namespace ClangCodeModel

View File

@@ -58,9 +58,6 @@ using namespace ClangCodeModel::Internal;
namespace {
QString qrcPath(const QByteArray relativeFilePath)
{ return QLatin1String(":/unittests/ClangCodeModel/") + QString::fromUtf8(relativeFilePath); }
CppTools::Tests::TemporaryDir *globalTemporaryDir()
{
static CppTools::Tests::TemporaryDir dir;

View File

@@ -0,0 +1,331 @@
/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "clangdtests.h"
#include "clangautomationutils.h"
#include "clangbatchfileprocessor.h"
#include "../clangdclient.h"
#include "../clangmodelmanagersupport.h"
#include <cplusplus/FindUsages.h>
#include <cpptools/cppcodemodelsettings.h>
#include <cpptools/cpptoolsreuse.h>
#include <cpptools/cpptoolstestcase.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/find/searchresultitem.h>
#include <projectexplorer/kitmanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <qtsupport/qtkitinformation.h>
#include <utils/algorithm.h>
#include <QEventLoop>
#include <QScopedPointer>
#include <QTimer>
#include <QtTest>
using namespace CPlusPlus;
using namespace Core;
using namespace ProjectExplorer;
namespace ClangCodeModel {
namespace Internal {
namespace Tests {
void ClangdTests::initTestCase()
{
const auto settings = CppTools::codeModelSettings();
const QString clangdFromEnv = qEnvironmentVariable("QTC_CLANGD");
if (!clangdFromEnv.isEmpty())
settings->setClangdFilePath(Utils::FilePath::fromString(clangdFromEnv));
const auto clangd = settings->clangdFilePath();
if (clangd.isEmpty() || !clangd.exists())
QSKIP("clangd binary not found");
settings->setUseClangd(true);
}
template <typename Signal> static bool waitForSignalOrTimeout(
const typename QtPrivate::FunctionPointer<Signal>::Object *sender, Signal signal)
{
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeOutInMs());
QEventLoop loop;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(sender, signal, &loop, &QEventLoop::quit);
timer.start();
loop.exec();
return timer.isActive();
}
// The main point here is to test our access type categorization.
// We do not try to stress-test clangd's "Find References" functionality; such tests belong
// into LLVM.
void ClangdTests::testFindReferences()
{
// Find suitable kit.
const QList<Kit *> allKits = KitManager::kits();
if (allKits.isEmpty())
QSKIP("This test requires at least one kit to be present");
Kit * const kit = Utils::findOr(allKits, nullptr, [](const Kit *k) {
return k->isValid() && QtSupport::QtKitAspect::qtVersion(k);
});
if (!kit)
QSKIP("The test requires at least one valid kit with a valid Qt");
// Copy project out of qrc file, open it, and set up target.
CppTools::Tests::TemporaryCopiedDir testDir(qrcPath("find-usages"));
QVERIFY(testDir.isValid());
const auto openProjectResult = ProjectExplorerPlugin::openProject(
testDir.absolutePath("find-usages.pro"));
QVERIFY2(openProjectResult, qPrintable(openProjectResult.errorMessage()));
openProjectResult.project()->configureAsExampleProject(kit);
// Setting up the project should result in a clangd client being created.
// Wait until that has happened.
const auto modelManagerSupport = ClangModelManagerSupport::instance();
ClangdClient *client = modelManagerSupport->clientForProject(openProjectResult.project());
if (!client) {
QVERIFY(waitForSignalOrTimeout(modelManagerSupport,
&ClangModelManagerSupport::createdClient));
client = modelManagerSupport->clientForProject(openProjectResult.project());
}
QVERIFY(client);
client->enableTesting();
// Wait until the client is fully initialized, i.e. it's completed the handshake
// with the server.
if (!client->reachable())
QVERIFY(waitForSignalOrTimeout(client, &ClangdClient::initialized));
QVERIFY(client->reachable());
// The kind of AST support we need was introduced in LLVM 13.
if (client->versionNumber() < QVersionNumber(13))
QSKIP("Find Usages test needs clang >= 13");
// Wait for index to build.
if (!client->isFullyIndexed())
QVERIFY(waitForSignalOrTimeout(client, &ClangdClient::indexingFinished));
QVERIFY(client->isFullyIndexed());
// Open cpp documents.
struct EditorCloser {
static void cleanup(IEditor *editor) { EditorManager::closeEditors({editor}); }
};
const auto headerPath = Utils::FilePath::fromString(testDir.absolutePath("defs.h"));
QVERIFY2(headerPath.exists(), qPrintable(headerPath.toUserOutput()));
QScopedPointer<IEditor, EditorCloser> headerEditor(
EditorManager::openEditor(headerPath.toString()));
QVERIFY(headerEditor);
const auto headerDoc = qobject_cast<TextEditor::TextDocument *>(headerEditor->document());
QVERIFY(headerDoc);
QVERIFY(client->documentForFilePath(headerPath) == headerDoc);
const auto cppFilePath = Utils::FilePath::fromString(testDir.absolutePath("main.cpp"));
QVERIFY2(cppFilePath.exists(), qPrintable(cppFilePath.toUserOutput()));
QScopedPointer<IEditor, EditorCloser> cppFileEditor(
EditorManager::openEditor(cppFilePath.toString()));
QVERIFY(cppFileEditor);
const auto cppDoc = qobject_cast<TextEditor::TextDocument *>(cppFileEditor->document());
QVERIFY(cppDoc);
QVERIFY(client->documentForFilePath(cppFilePath) == cppDoc);
// ... and we're ready to go.
QList<SearchResultItem> searchResults;
connect(client, &ClangdClient::foundReferences, this,
[&searchResults](const QList<SearchResultItem> &results) {
if (results.isEmpty())
return;
if (results.first().path().first().endsWith("defs.h"))
searchResults = results + searchResults; // Guarantee expected file order.
else
searchResults += results;
});
#define FIND_USAGES(doc, pos) do { \
QTextCursor cursor((doc)->document()); \
cursor.setPosition((pos)); \
searchResults.clear(); \
client->findUsages((doc), cursor); \
QVERIFY(waitForSignalOrTimeout(client, &ClangdClient::findUsagesDone)); \
} while (false)
#define EXPECT_RESULT(index, lne, col, type) \
QCOMPARE(searchResults.at(index).mainRange().begin.line, lne); \
QCOMPARE(searchResults.at(index).mainRange().begin.column, col); \
QCOMPARE(searchResults.at(index).userData().toInt(), int(type))
// All kinds of checks involving a struct member.
FIND_USAGES(headerDoc, 55);
QCOMPARE(searchResults.size(), 32);
EXPECT_RESULT(0, 2, 17, Usage::Type::Read);
EXPECT_RESULT(1, 3, 15, Usage::Type::Declaration);
EXPECT_RESULT(2, 6, 17, Usage::Type::WritableRef);
EXPECT_RESULT(3, 8, 11, Usage::Type::WritableRef);
EXPECT_RESULT(4, 9, 13, Usage::Type::WritableRef);
EXPECT_RESULT(5, 10, 12, Usage::Type::WritableRef);
EXPECT_RESULT(6, 11, 13, Usage::Type::WritableRef);
EXPECT_RESULT(7, 12, 14, Usage::Type::WritableRef);
EXPECT_RESULT(8, 13, 26, Usage::Type::WritableRef);
EXPECT_RESULT(9, 14, 23, Usage::Type::Read);
EXPECT_RESULT(10, 15, 14, Usage::Type::Read);
EXPECT_RESULT(11, 16, 24, Usage::Type::WritableRef);
EXPECT_RESULT(12, 17, 15, Usage::Type::WritableRef);
EXPECT_RESULT(13, 18, 22, Usage::Type::Read);
EXPECT_RESULT(14, 19, 12, Usage::Type::WritableRef);
EXPECT_RESULT(15, 20, 12, Usage::Type::Read);
EXPECT_RESULT(16, 21, 13, Usage::Type::WritableRef);
EXPECT_RESULT(17, 22, 13, Usage::Type::Read);
EXPECT_RESULT(18, 23, 12, Usage::Type::Read);
EXPECT_RESULT(19, 42, 20, Usage::Type::Read);
EXPECT_RESULT(20, 44, 15, Usage::Type::Read);
EXPECT_RESULT(21, 47, 15, Usage::Type::Write);
EXPECT_RESULT(22, 50, 11, Usage::Type::Read);
EXPECT_RESULT(23, 51, 11, Usage::Type::Write);
EXPECT_RESULT(24, 52, 9, Usage::Type::Write);
EXPECT_RESULT(25, 53, 7, Usage::Type::Write);
EXPECT_RESULT(26, 56, 7, Usage::Type::Write);
EXPECT_RESULT(27, 56, 25, Usage::Type::Other);
EXPECT_RESULT(28, 58, 13, Usage::Type::Read);
EXPECT_RESULT(29, 58, 25, Usage::Type::Read);
EXPECT_RESULT(30, 59, 7, Usage::Type::Write);
EXPECT_RESULT(31, 59, 24, Usage::Type::Read);
// Detect constructor member initialization as a write access.
FIND_USAGES(headerDoc, 68);
QCOMPARE(searchResults.size(), 2);
EXPECT_RESULT(0, 2, 10, Usage::Type::Write);
EXPECT_RESULT(1, 4, 8, Usage::Type::Declaration);
// Detect direct member initialization.
FIND_USAGES(headerDoc, 101);
QCOMPARE(searchResults.size(), 2);
EXPECT_RESULT(0, 5, 21, Usage::Type::Initialization);
EXPECT_RESULT(1, 45, 16, Usage::Type::Read);
// Make sure that pure virtual declaration is not mistaken for an assignment.
FIND_USAGES(headerDoc, 420);
QCOMPARE(searchResults.size(), 3); // FIXME: The override gets reported twice. clang bug?
EXPECT_RESULT(0, 17, 17, Usage::Type::Declaration);
EXPECT_RESULT(1, 21, 9, Usage::Type::Declaration);
EXPECT_RESULT(2, 21, 9, Usage::Type::Declaration);
// References to pointer variable.
FIND_USAGES(cppDoc, 52);
QCOMPARE(searchResults.size(), 11);
EXPECT_RESULT(0, 6, 10, Usage::Type::Initialization);
EXPECT_RESULT(1, 8, 4, Usage::Type::Write);
EXPECT_RESULT(2, 10, 4, Usage::Type::Write);
EXPECT_RESULT(3, 24, 5, Usage::Type::Write);
EXPECT_RESULT(4, 25, 11, Usage::Type::WritableRef);
EXPECT_RESULT(5, 26, 11, Usage::Type::Read);
EXPECT_RESULT(6, 27, 10, Usage::Type::WritableRef);
EXPECT_RESULT(7, 28, 10, Usage::Type::Read);
EXPECT_RESULT(8, 29, 11, Usage::Type::Read);
EXPECT_RESULT(9, 30, 15, Usage::Type::WritableRef);
EXPECT_RESULT(10, 31, 22, Usage::Type::Read);
// References to struct variable, directly and via members.
FIND_USAGES(cppDoc, 39);
QCOMPARE(searchResults.size(), 34);
EXPECT_RESULT(0, 5, 7, Usage::Type::Declaration);
EXPECT_RESULT(1, 6, 15, Usage::Type::WritableRef);
EXPECT_RESULT(2, 8, 9, Usage::Type::WritableRef);
EXPECT_RESULT(3, 9, 11, Usage::Type::WritableRef);
EXPECT_RESULT(4, 11, 4, Usage::Type::Write);
EXPECT_RESULT(5, 11, 11, Usage::Type::WritableRef);
EXPECT_RESULT(6, 12, 12, Usage::Type::WritableRef);
EXPECT_RESULT(7, 13, 6, Usage::Type::Write);
EXPECT_RESULT(8, 14, 21, Usage::Type::Read);
EXPECT_RESULT(9, 15, 4, Usage::Type::Write);
EXPECT_RESULT(10, 15, 12, Usage::Type::Read);
EXPECT_RESULT(11, 16, 22, Usage::Type::WritableRef);
EXPECT_RESULT(12, 17, 13, Usage::Type::WritableRef);
EXPECT_RESULT(13, 18, 20, Usage::Type::Read);
EXPECT_RESULT(14, 19, 10, Usage::Type::WritableRef);
EXPECT_RESULT(15, 20, 10, Usage::Type::Read);
EXPECT_RESULT(16, 21, 11, Usage::Type::WritableRef);
EXPECT_RESULT(17, 22, 11, Usage::Type::Read);
EXPECT_RESULT(18, 23, 10, Usage::Type::Read);
EXPECT_RESULT(19, 32, 4, Usage::Type::Write);
EXPECT_RESULT(20, 33, 23, Usage::Type::WritableRef);
EXPECT_RESULT(21, 34, 23, Usage::Type::Read);
EXPECT_RESULT(22, 35, 15, Usage::Type::WritableRef);
EXPECT_RESULT(23, 36, 22, Usage::Type::WritableRef);
EXPECT_RESULT(24, 37, 4, Usage::Type::Read);
EXPECT_RESULT(25, 38, 4, Usage::Type::WritableRef);
EXPECT_RESULT(26, 39, 6, Usage::Type::WritableRef);
EXPECT_RESULT(27, 40, 4, Usage::Type::Read);
EXPECT_RESULT(28, 41, 4, Usage::Type::WritableRef);
EXPECT_RESULT(29, 42, 4, Usage::Type::Read);
EXPECT_RESULT(30, 42, 18, Usage::Type::Read);
EXPECT_RESULT(31, 43, 11, Usage::Type::Write);
EXPECT_RESULT(32, 54, 4, Usage::Type::Other);
EXPECT_RESULT(33, 55, 4, Usage::Type::Other);
// References to struct type.
FIND_USAGES(headerDoc, 7);
QCOMPARE(searchResults.size(), 18);
EXPECT_RESULT(0, 1, 7, Usage::Type::Declaration);
EXPECT_RESULT(1, 2, 4, Usage::Type::Declaration);
EXPECT_RESULT(2, 20, 19, Usage::Type::Other);
// These are conceptually questionable, as S is a type and thus we cannot "read from"
// or "write to" it. But it probably matches the intuitive user expectation.
EXPECT_RESULT(3, 10, 9, Usage::Type::WritableRef);
EXPECT_RESULT(4, 12, 4, Usage::Type::Write);
EXPECT_RESULT(5, 44, 12, Usage::Type::Read);
EXPECT_RESULT(6, 45, 13, Usage::Type::Read);
EXPECT_RESULT(7, 47, 12, Usage::Type::Write);
EXPECT_RESULT(8, 50, 8, Usage::Type::Read);
EXPECT_RESULT(9, 51, 8, Usage::Type::Write);
EXPECT_RESULT(10, 52, 6, Usage::Type::Write);
EXPECT_RESULT(11, 53, 4, Usage::Type::Write);
EXPECT_RESULT(12, 56, 4, Usage::Type::Write);
EXPECT_RESULT(13, 56, 22, Usage::Type::Other);
EXPECT_RESULT(14, 58, 10, Usage::Type::Read);
EXPECT_RESULT(15, 58, 22, Usage::Type::Read);
EXPECT_RESULT(16, 59, 4, Usage::Type::Write);
EXPECT_RESULT(17, 59, 21, Usage::Type::Read);
// References to subclass type.
FIND_USAGES(headerDoc, 450);
QCOMPARE(searchResults.size(), 4);
EXPECT_RESULT(0, 20, 7, Usage::Type::Declaration);
EXPECT_RESULT(1, 5, 4, Usage::Type::Other);
EXPECT_RESULT(2, 13, 21, Usage::Type::Other);
EXPECT_RESULT(3, 32, 8, Usage::Type::Other);
// References to array variables.
FIND_USAGES(cppDoc, 1134);
QCOMPARE(searchResults.size(), 3);
EXPECT_RESULT(0, 57, 8, Usage::Type::Declaration);
EXPECT_RESULT(1, 58, 4, Usage::Type::Write);
EXPECT_RESULT(2, 59, 15, Usage::Type::Read);
}
} // namespace Tests
} // namespace Internal
} // namespace ClangCodeModel

View File

@@ -0,0 +1,47 @@
/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include <QObject>
namespace ClangCodeModel {
namespace Internal {
namespace Tests {
class ClangdTests : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testFindReferences();
};
} // namespace Tests
} // namespace Internal
} // namespace ClangCodeModel

View File

@@ -29,5 +29,8 @@
<file>membercompletion-friend.cpp</file>
<file>functionCompletionFiltered.cpp</file>
<file>functionCompletionFiltered2.cpp</file>
<file>find-usages/defs.h</file>
<file>find-usages/main.cpp</file>
<file>find-usages/find-usages.pro</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,28 @@
struct S {
S() : value2(value) {}
static int value;
int value2 : 2;
static const int value3 = 0;
static void *p;
static const void *p2;
struct Nested {
int constFunc() const;
void constFunc(int) const;
void nonConstFunc();
} n;
Nested constFunc() const;
void nonConstFunc();
static void staticFunc1() {}
static void staticFunc2();
virtual void pureVirtual() = 0;
};
struct S2 : public S {
void pureVirtual() override {}
};
void func1(int &);
void func2(const int &);
void func3(int *);
void func4(const int *);
void func5(int);

View File

@@ -0,0 +1,4 @@
TEMPLATE = app
QT = core
HEADERS = defs.h
SOURCES = main.cpp

View File

@@ -0,0 +1,60 @@
#include "defs.h"
int main()
{
S2 s;
auto *p = &s.value;
int **pp;
p = &s.value;
*pp = &s.value;
p = &S::value;
s.p = &s.value;
S::p = &s.value;
(&s)->p = &((new S2)->value);
const int *p2 = &s.value;
s.p2 = &s.value;
int * const p3 = &s.value;
int &r = s.value;
const int &cr = s.value;
func1(s.value);
func2(s.value);
func3(&s.value);
func4(&s.value);
func5(s.value);
*p = 5;
func1(*p);
func2(*p);
func3(p);
func4(p);
func5(*p);
int &r2 = *p;
const int &cr2 = *p;
s = S2();
auto * const ps = &s;
const auto *ps2 = &s;
auto &pr = s;
const auto pr2 = &s;
s.constFunc().nonConstFunc();
s.nonConstFunc();
(&s)->nonConstFunc();
s.n.constFunc();
s.n.nonConstFunc();
s.n.constFunc(s.value);
delete s.p;
switch (S::value) {
case S::value3: break;
}
switch (S::value = 5) {
default: break;
}
if (S::value) {}
if (S::value = 0) {}
++S::value;
S::value--;
s.staticFunc1();
s.staticFunc2();
S::value = sizeof S::value;
int array[3];
array[S::value] = S::value;
S::value = array[S::value];
}