/**************************************************************************** ** ** 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 "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; 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(), {"--background-index", "--limit-results=0"}}; if (!jsonDbDir.isEmpty()) cmd.addArg("--compile-commands-dir=" + jsonDbDir.toString()); if (clangdLog().isDebugEnabled()) cmd.addArgs({"--log=verbose", "--pretty"}); const auto interface = new StdIOClientInterface; interface->setCommandLine(cmd); 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)), d(new Private(this)) { setName(tr("clangd")); LanguageFilter langFilter; langFilter.mimeTypes = QStringList{"text/x-chdr", "text/x-c++hdr", "text/x-c++src", "text/x-objc++src", "text/x-objcsrc"}; setSupportedLanguage(langFilter); LanguageServerProtocol::ClientCapabilities caps = Client::defaultClientCapabilities(); caps.clearExperimental(); caps.clearTextDocument(); setClientCapabilities(caps); setLocatorsEnabled(false); setProgressTitleForToken(indexingToken(), tr("Parsing C/C++ Files (clangd)")); setCurrentProject(project); connect(this, &Client::workDone, this, [this](const ProgressToken &token) { const QString * const val = Utils::get_if(&token); 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