From 6dc3c1f1563a3d5979fa7408c07e950be280827e Mon Sep 17 00:00:00 2001 From: Christian Kandeler Date: Wed, 21 Apr 2021 14:29:49 +0200 Subject: [PATCH] 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 Reviewed-by: David Schulz --- src/plugins/clangcodemodel/CMakeLists.txt | 3 +- src/plugins/clangcodemodel/clangcodemodel.pro | 2 + src/plugins/clangcodemodel/clangcodemodel.qbs | 3 + .../clangcodemodel_dependencies.pri | 2 + .../clangcodemodel/clangcodemodelplugin.cpp | 2 + src/plugins/clangcodemodel/clangdclient.cpp | 525 +++++++++++++++++- src/plugins/clangcodemodel/clangdclient.h | 23 +- .../clangmodelmanagersupport.cpp | 21 +- .../clangcodemodel/clangmodelmanagersupport.h | 3 + .../clangcodemodel/clangrefactoringengine.cpp | 4 +- .../test/clangautomationutils.cpp | 5 + .../test/clangautomationutils.h | 2 + .../test/clangcodecompletion_test.cpp | 3 - .../clangcodemodel/test/clangdtests.cpp | 331 +++++++++++ src/plugins/clangcodemodel/test/clangdtests.h | 47 ++ .../test/data/clangtestdata.qrc | 3 + .../test/data/find-usages/defs.h | 28 + .../test/data/find-usages/find-usages.pro | 4 + .../test/data/find-usages/main.cpp | 60 ++ 19 files changed, 1042 insertions(+), 29 deletions(-) create mode 100644 src/plugins/clangcodemodel/test/clangdtests.cpp create mode 100644 src/plugins/clangcodemodel/test/clangdtests.h create mode 100644 src/plugins/clangcodemodel/test/data/find-usages/defs.h create mode 100644 src/plugins/clangcodemodel/test/data/find-usages/find-usages.pro create mode 100644 src/plugins/clangcodemodel/test/data/find-usages/main.cpp diff --git a/src/plugins/clangcodemodel/CMakeLists.txt b/src/plugins/clangcodemodel/CMakeLists.txt index c8c8d9ec7fd..6da3ac14b4d 100644 --- a/src/plugins/clangcodemodel/CMakeLists.txt +++ b/src/plugins/clangcodemodel/CMakeLists.txt @@ -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 ) diff --git a/src/plugins/clangcodemodel/clangcodemodel.pro b/src/plugins/clangcodemodel/clangcodemodel.pro index 22623b7c16d..f6d7b79071d 100644 --- a/src/plugins/clangcodemodel/clangcodemodel.pro +++ b/src/plugins/clangcodemodel/clangcodemodel.pro @@ -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/*) diff --git a/src/plugins/clangcodemodel/clangcodemodel.qbs b/src/plugins/clangcodemodel/clangcodemodel.qbs index d291d50fc0d..d16c11f7326 100644 --- a/src/plugins/clangcodemodel/clangcodemodel.qbs +++ b/src/plugins/clangcodemodel/clangcodemodel.qbs @@ -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", ] } diff --git a/src/plugins/clangcodemodel/clangcodemodel_dependencies.pri b/src/plugins/clangcodemodel/clangcodemodel_dependencies.pri index eb6635835be..f32eaa4c4f4 100644 --- a/src/plugins/clangcodemodel/clangcodemodel_dependencies.pri +++ b/src/plugins/clangcodemodel/clangcodemodel_dependencies.pri @@ -10,3 +10,5 @@ QTC_PLUGIN_DEPENDS += \ QTC_TEST_DEPENDS += \ cppeditor \ qmakeprojectmanager + +equals(TEST, 1): QTC_PLUGIN_DEPENDS += qtsupport diff --git a/src/plugins/clangcodemodel/clangcodemodelplugin.cpp b/src/plugins/clangcodemodel/clangcodemodelplugin.cpp index 81c596b9092..126b3e9b5b6 100644 --- a/src/plugins/clangcodemodel/clangcodemodelplugin.cpp +++ b/src/plugins/clangcodemodel/clangcodemodelplugin.cpp @@ -32,6 +32,7 @@ #ifdef WITH_TESTS # include "test/clangbatchfileprocessor.h" # include "test/clangcodecompletion_test.h" +# include "test/clangdtests.h" #endif #include @@ -204,6 +205,7 @@ QVector ClangCodeModelPlugin::createTestObjects() const { return { new Tests::ClangCodeCompletionTest, + new Tests::ClangdTests, }; } #endif diff --git a/src/plugins/clangcodemodel/clangdclient.cpp b/src/plugins/clangcodemodel/clangdclient.cpp index aef95e7d52b..c7e543c439d 100644 --- a/src/plugins/clangcodemodel/clangdclient.cpp +++ b/src/plugins/clangcodemodel/clangdclient.cpp @@ -25,10 +25,21 @@ #include "clangdclient.h" +#include +#include +#include #include +#include #include #include +#include +#include +#include +#include + +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(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() const { return optionalValue(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 clang’s base AST node type, + // such as Expr. The most common are “expression”, “statement”, “type” and “declaration”. + QString role() const { return typedValue(roleKey); } + + // The specific kind of node, such as “BinaryOperator”. Corresponds to clang’s concrete + // node class, with Expr etc suffix dropped. + QString kind() const { return typedValue(kindKey); } + + // Brief additional details, such as ‘||’. Information present here depends on the node kind. + Utils::optional detail() const { return optionalValue(detailKey); } + + // One line dump of information, similar to that printed by clang -Xclang -ast-dump. + // Only available for certain types of nodes. + Utils::optional arcana() const { return optionalValue(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(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> children() const { return optionalArray(childrenKey); } + + bool hasRange() const { return contains(rangeKey); } + + bool arcanaContains(const QString &s) const + { + const Utils::optional arcanaString = arcana(); + return arcanaString && arcanaString->contains(s); + } + + bool detailIs(const QString &s) const + { + return detail() && detail().value() == s; + } + + QString type() const + { + const Utils::optional 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> 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 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 getAstPath(const AstNode &root, const Range &range) +{ + QList path; + QList 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 &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 +{ +public: + using Request::Request; + explicit AstRequest(const AstParams ¶ms) : 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> rangesAndLineText; + QString fileContent; + AstNode ast; +}; +class ReferencesData { +public: + void setCanceled() { search->setUserData(true); } + bool isCanceled() const { return search && search->userData().toBool(); } + + QMap fileData; + QList pendingAstRequests; + QPointer search; + quint64 key; +}; + +class ClangdClient::Private +{ +public: + Private(ClangdClient *q) : q(q) {} + + void handleFindUsagesResult(quint64 key, const QList &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 runningFindUsages; + Utils::optional 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(&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 requestId = symbolSupport().findUsages( + document, cursor, [this, key = refData.key](const QList &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 &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 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 diff --git a/src/plugins/clangcodemodel/clangdclient.h b/src/plugins/clangcodemodel/clangdclient.h index f639419bbd9..f5d7c022f72 100644 --- a/src/plugins/clangcodemodel/clangdclient.h +++ b/src/plugins/clangcodemodel/clangdclient.h @@ -27,7 +27,11 @@ #include +#include + +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 &items); + void findUsagesDone(); private: - bool m_isFullyIndexed = false; + class Private; + Private * const d; }; } // namespace Internal diff --git a/src/plugins/clangcodemodel/clangmodelmanagersupport.cpp b/src/plugins/clangcodemodel/clangmodelmanagersupport.cpp index 8215e0ba8a2..e95bcb71d9a 100644 --- a/src/plugins/clangcodemodel/clangmodelmanagersupport.cpp +++ b/src/plugins/clangcodemodel/clangmodelmanagersupport.cpp @@ -64,7 +64,6 @@ #include #include -#include #include #include #include @@ -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) diff --git a/src/plugins/clangcodemodel/clangmodelmanagersupport.h b/src/plugins/clangcodemodel/clangmodelmanagersupport.h index 16f84e71637..21da8e1aea5 100644 --- a/src/plugins/clangcodemodel/clangmodelmanagersupport.h +++ b/src/plugins/clangcodemodel/clangmodelmanagersupport.h @@ -83,6 +83,9 @@ public: static ClangModelManagerSupport *instance(); +signals: + void createdClient(ClangdClient *client); + private: void onEditorOpened(Core::IEditor *editor); void onEditorClosed(const QList &editors); diff --git a/src/plugins/clangcodemodel/clangrefactoringengine.cpp b/src/plugins/clangcodemodel/clangrefactoringengine.cpp index 0328bc89ba0..dc0e06899f7 100644 --- a/src/plugins/clangcodemodel/clangrefactoringengine.cpp +++ b/src/plugins/clangcodemodel/clangrefactoringengine.cpp @@ -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 diff --git a/src/plugins/clangcodemodel/test/clangautomationutils.cpp b/src/plugins/clangcodemodel/test/clangautomationutils.cpp index 269b29acd44..45030f7fbaf 100644 --- a/src/plugins/clangcodemodel/test/clangautomationutils.cpp +++ b/src/plugins/clangcodemodel/test/clangautomationutils.cpp @@ -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 diff --git a/src/plugins/clangcodemodel/test/clangautomationutils.h b/src/plugins/clangcodemodel/test/clangautomationutils.h index 4e16db9f2db..dcb62eda768 100644 --- a/src/plugins/clangcodemodel/test/clangautomationutils.h +++ b/src/plugins/clangcodemodel/test/clangautomationutils.h @@ -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 diff --git a/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp b/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp index c4dd8e882ec..a0d8aef4639 100644 --- a/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp +++ b/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp @@ -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; diff --git a/src/plugins/clangcodemodel/test/clangdtests.cpp b/src/plugins/clangcodemodel/test/clangdtests.cpp new file mode 100644 index 00000000000..0145f79cdbb --- /dev/null +++ b/src/plugins/clangcodemodel/test/clangdtests.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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 static bool waitForSignalOrTimeout( + const typename QtPrivate::FunctionPointer::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 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 headerEditor( + EditorManager::openEditor(headerPath.toString())); + QVERIFY(headerEditor); + const auto headerDoc = qobject_cast(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 cppFileEditor( + EditorManager::openEditor(cppFilePath.toString())); + QVERIFY(cppFileEditor); + const auto cppDoc = qobject_cast(cppFileEditor->document()); + QVERIFY(cppDoc); + QVERIFY(client->documentForFilePath(cppFilePath) == cppDoc); + + // ... and we're ready to go. + QList searchResults; + connect(client, &ClangdClient::foundReferences, this, + [&searchResults](const QList &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 diff --git a/src/plugins/clangcodemodel/test/clangdtests.h b/src/plugins/clangcodemodel/test/clangdtests.h new file mode 100644 index 00000000000..ddf2c1b1d27 --- /dev/null +++ b/src/plugins/clangcodemodel/test/clangdtests.h @@ -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 + +namespace ClangCodeModel { +namespace Internal { +namespace Tests { + +class ClangdTests : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void testFindReferences(); +}; + +} // namespace Tests +} // namespace Internal +} // namespace ClangCodeModel + diff --git a/src/plugins/clangcodemodel/test/data/clangtestdata.qrc b/src/plugins/clangcodemodel/test/data/clangtestdata.qrc index d1013a7d29a..073e9fa309e 100644 --- a/src/plugins/clangcodemodel/test/data/clangtestdata.qrc +++ b/src/plugins/clangcodemodel/test/data/clangtestdata.qrc @@ -29,5 +29,8 @@ membercompletion-friend.cpp functionCompletionFiltered.cpp functionCompletionFiltered2.cpp + find-usages/defs.h + find-usages/main.cpp + find-usages/find-usages.pro diff --git a/src/plugins/clangcodemodel/test/data/find-usages/defs.h b/src/plugins/clangcodemodel/test/data/find-usages/defs.h new file mode 100644 index 00000000000..8480e6f6e52 --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/find-usages/defs.h @@ -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); diff --git a/src/plugins/clangcodemodel/test/data/find-usages/find-usages.pro b/src/plugins/clangcodemodel/test/data/find-usages/find-usages.pro new file mode 100644 index 00000000000..c4474855308 --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/find-usages/find-usages.pro @@ -0,0 +1,4 @@ +TEMPLATE = app +QT = core +HEADERS = defs.h +SOURCES = main.cpp diff --git a/src/plugins/clangcodemodel/test/data/find-usages/main.cpp b/src/plugins/clangcodemodel/test/data/find-usages/main.cpp new file mode 100644 index 00000000000..36941dc4735 --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/find-usages/main.cpp @@ -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]; +}