aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Kandeler <christian.kandeler@qt.io>2024-01-25 13:08:00 +0100
committerChristian Kandeler <christian.kandeler@qt.io>2024-02-08 10:15:27 +0000
commitf011142307340444e5ad03b8320bc30ba3bb7982 (patch)
tree345cfb0a7f906ace27734a85e15e190680209cee
parent358e1759b33d8715124d2eac9dcff34f3172476e (diff)
Language server: Add completion support
This feature uncovered some sloppiness in our handling of QVariant types, which has now been fixed. Task-number: QBS-395 Change-Id: I687cef7470d97fe7887e4a7a1dbe672b2b9c79ec Reviewed-by: Ivan Komissarov <ABBAPOH@gmail.com> Reviewed-by: David Schulz <david.schulz@qt.io>
-rw-r--r--src/app/qbs/lspserver.cpp184
-rw-r--r--src/app/qbs/lspserver.h9
-rw-r--r--src/app/qbs/session.cpp2
-rw-r--r--src/lib/corelib/api/projectdata.cpp73
-rw-r--r--src/lib/corelib/api/projectdata.h10
-rw-r--r--src/lib/corelib/buildgraph/buildgraph.cpp4
-rw-r--r--src/lib/corelib/buildgraph/buildgraphloader.cpp15
-rw-r--r--src/lib/corelib/buildgraph/rulecommands.cpp14
-rw-r--r--src/lib/corelib/buildgraph/rulesapplicator.cpp11
-rw-r--r--src/lib/corelib/buildgraph/transformerchangetracking.cpp2
-rw-r--r--src/lib/corelib/language/language.cpp19
-rw-r--r--src/lib/corelib/language/propertydeclaration.cpp20
-rw-r--r--src/lib/corelib/language/propertydeclaration.h1
-rw-r--r--src/lib/corelib/language/propertymapinternal.h2
-rw-r--r--src/lib/corelib/language/scriptengine.cpp2
-rw-r--r--src/lib/corelib/loader/dependenciesresolver.cpp2
-rw-r--r--src/lib/corelib/loader/loaderutils.cpp2
-rw-r--r--src/lib/corelib/loader/probesresolver.cpp10
-rw-r--r--src/lib/corelib/loader/productresolver.cpp6
-rw-r--r--src/lib/corelib/parser/qmljsastvisitor_p.h2
-rw-r--r--src/lib/corelib/parser/qmljsengine_p.h2
-rw-r--r--src/lib/corelib/parser/qmljsglobal_p.h35
-rw-r--r--src/lib/corelib/parser/qmljslexer_p.h2
-rw-r--r--src/lib/corelib/parser/qmljsparser_p.h2
-rw-r--r--src/lib/corelib/tools/persistence.cpp8
-rw-r--r--src/lib/corelib/tools/persistence.h18
-rw-r--r--src/lib/corelib/tools/qttools.h45
-rw-r--r--src/lib/corelib/tools/setupprojectparameters.cpp3
-rw-r--r--tests/auto/api/tst_api.cpp3
-rw-r--r--tests/auto/blackbox/testdata/lsp/lsp.qbs1
-rw-r--r--tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs3
-rw-r--r--tests/auto/blackbox/tst_blackbox.cpp37
-rw-r--r--tests/auto/language/tst_language.cpp2
-rw-r--r--tests/lspclient/lspclient.cpp46
34 files changed, 480 insertions, 117 deletions
diff --git a/src/app/qbs/lspserver.cpp b/src/app/qbs/lspserver.cpp
index 807b6fb1d..c6cce6706 100644
--- a/src/app/qbs/lspserver.cpp
+++ b/src/app/qbs/lspserver.cpp
@@ -39,18 +39,25 @@
#include "lspserver.h"
+#include <api/projectdata.h>
#include <logging/translator.h>
#include <lsp/basemessage.h>
+#include <lsp/completion.h>
#include <lsp/initializemessages.h>
#include <lsp/jsonrpcmessages.h>
#include <lsp/messages.h>
+#include <lsp/servercapabilities.h>
#include <lsp/textsynchronization.h>
+#include <parser/qmljsastvisitor_p.h>
+#include <parser/qmljslexer_p.h>
+#include <parser/qmljsparser_p.h>
#include <tools/qbsassert.h>
#include <tools/stlutils.h>
#include <QBuffer>
#include <QLocalServer>
#include <QLocalSocket>
+#include <QMap>
#include <unordered_map>
#ifdef Q_OS_WINDOWS
@@ -103,6 +110,32 @@ static int posToOffset(const lsp::Position &pos, const QString &doc)
return posToOffset(posFromLspPos(pos), doc);
}
+class AstNodeLocator : public QbsQmlJS::AST::Visitor
+{
+public:
+ AstNodeLocator(int position, QbsQmlJS::AST::UiProgram &ast)
+ : m_position(position)
+ {
+ ast.accept(this);
+ }
+
+ QList<QbsQmlJS::AST::Node *> path() const { return m_path; }
+
+private:
+ bool preVisit(QbsQmlJS::AST::Node *node) override
+ {
+ if (int(node->firstSourceLocation().offset) > m_position)
+ return false;
+ if (int(node->lastSourceLocation().offset) < m_position)
+ return false;
+ m_path << node;
+ return true;
+ }
+
+ const int m_position;
+ QList<QbsQmlJS::AST::Node *> m_path;
+};
+
class LspServer::Private
{
public:
@@ -119,6 +152,7 @@ public:
void handleInitializeRequest();
void handleInitializedNotification();
void handleGotoDefinitionRequest();
+ void handleCompletionRequest();
void handleShutdownRequest();
void handleDidOpenNotification();
void handleDidChangeNotification();
@@ -130,6 +164,7 @@ public:
lsp::BaseMessage currentMessage;
QJsonObject messageObject;
QLocalSocket *socket = nullptr;
+ ProjectData projectData;
CodeLinks codeLinks;
std::unordered_map<QString, Document> documents;
@@ -153,8 +188,9 @@ LspServer::LspServer() : d(new Private)
LspServer::~LspServer() { delete d; }
-void LspServer::updateProjectData(const CodeLinks &codeLinks)
+void LspServer::updateProjectData(const ProjectData &projectData, const CodeLinks &codeLinks)
{
+ d->projectData = projectData;
d->codeLinks = codeLinks;
}
@@ -271,6 +307,8 @@ void LspServer::Private::handleCurrentMessage()
return handleDidCloseNotification();
if (method == "textDocument/definition")
return handleGotoDefinitionRequest();
+ if (method == "textDocument/completion")
+ return handleCompletionRequest();
sendErrorResponse(LspErrorResponse::MethodNotFound, Tr::tr("This server can do very little."));
}
@@ -291,6 +329,9 @@ void LspServer::Private::handleInitializeRequest()
lsp::ServerCapabilities capabilities; // TODO: hover
capabilities.setDefinitionProvider(true);
capabilities.setTextDocumentSync({int(lsp::TextDocumentSyncKind::Incremental)});
+ lsp::ServerCapabilities::CompletionOptions completionOptions;
+ completionOptions.setTriggerCharacters({"."});
+ capabilities.setCompletionProvider(completionOptions);
result.setCapabilities(capabilities);
sendResponse(result);
}
@@ -353,6 +394,142 @@ void LspServer::Private::handleGotoDefinitionRequest()
sendResponse(nullptr);
}
+// We operate under the assumption that the client has basic QML support.
+// Therefore, we only provide completion for qbs modules and their properties.
+// Only a simple prefix match is implemented, with no regard to the contents after the cursor.
+void LspServer::Private::handleCompletionRequest()
+{
+ if (!projectData.isValid())
+ return sendResponse(nullptr);
+
+ const lsp::CompletionParams params(messageObject.value(lsp::paramsKey));
+ const QString sourceFile = params.textDocument().uri().toLocalFile();
+ const Document *sourceDoc = nullptr;
+ if (const auto it = documents.find(sourceFile); it != documents.end())
+ sourceDoc = &it->second;
+ if (!sourceDoc)
+ return sendResponse(nullptr);
+
+ // If there are products corresponding to this file, check only these when looking for modules.
+ // Otherwise, check all products.
+ const QList<ProductData> allProducts = projectData.allProducts();
+ if (allProducts.isEmpty())
+ return sendResponse(nullptr);
+ QList<ProductData> relevantProducts;
+ for (const ProductData &p : allProducts) {
+ if (p.location().filePath() == sourceFile)
+ relevantProducts << p;
+ }
+ if (relevantProducts.isEmpty())
+ relevantProducts = allProducts;
+
+ QString identifierPrefix;
+ QStringList modulePrefixes;
+ const int offset = posToOffset(params.position(), sourceDoc->currentContent) - 1;
+ if (offset < 0 || offset >= sourceDoc->currentContent.length())
+ return sendResponse(nullptr);
+ const auto collectFromRawString = [&] {
+ int currentPos = offset;
+ const auto constructIdentifier = [&] {
+ QString id;
+ while (currentPos >= 0) {
+ const QChar c = sourceDoc->currentContent.at(currentPos);
+ if (!c.isLetterOrNumber() && c != '_')
+ break;
+ id.prepend(c);
+ --currentPos;
+ }
+ return id;
+ };
+ identifierPrefix = constructIdentifier();
+ while (true) {
+ if (currentPos <= 0 || sourceDoc->currentContent.at(currentPos) != '.')
+ return;
+ --currentPos;
+ const QString modulePrefix = constructIdentifier();
+ if (modulePrefix.isEmpty())
+ return;
+ modulePrefixes.prepend(modulePrefix);
+ }
+ };
+
+ // Parse the current file. Note that completion usually happens on invalid code, which
+ // often confuses the parser so much that it yields unusable results. Therefore, we always
+ // gather our input parameters from the raw string. We only use the parse result to skip
+ // completion in contexts where it is undesirable.
+ QbsQmlJS::Engine engine;
+ QbsQmlJS::Lexer lexer(&engine);
+ lexer.setCode(sourceDoc->currentContent, 1);
+ QbsQmlJS::Parser parser(&engine);
+ parser.parse();
+ if (parser.ast()) {
+ AstNodeLocator locator(offset, *parser.ast());
+ const QList<QbsQmlJS::AST::Node *> &astPath = locator.path();
+ if (!astPath.isEmpty()) {
+ switch (astPath.last()->kind) {
+ case QbsQmlJS::AST::Node::Kind_FieldMemberExpression:
+ case QbsQmlJS::AST::Node::Kind_UiObjectDefinition:
+ case QbsQmlJS::AST::Node::Kind_UiQualifiedId:
+ case QbsQmlJS::AST::Node::Kind_UiScriptBinding:
+ break;
+ default:
+ return sendResponse(nullptr);
+ }
+ }
+ }
+
+ collectFromRawString();
+ if (modulePrefixes.isEmpty() && identifierPrefix.isEmpty())
+ return sendResponse(nullptr); // We do not want to start completion from nothing.
+
+ QJsonArray results;
+ QMap<QString, QString> namesAndTypes;
+ for (const ProductData &product : std::as_const(relevantProducts)) {
+ const PropertyMap &moduleProps = product.moduleProperties();
+ const QStringList allModules = moduleProps.allModules();
+ const QString moduleNameOrPrefix = modulePrefixes.join('.');
+
+ // Case 1: Prefixes match a module name. Identifier can only expand to the name
+ // of a module property.
+ // Example: "Qt.core.a^" -> "Qt.core.availableBuildVariants"
+ if (!modulePrefixes.isEmpty() && allModules.contains(moduleNameOrPrefix)) {
+ for (const PropertyMap::PropertyInfo &info :
+ moduleProps.allPropertiesForModule(moduleNameOrPrefix)) {
+ if (info.isBuiltin)
+ continue;
+ if (!identifierPrefix.isEmpty() && !info.name.startsWith(identifierPrefix))
+ continue;
+ namesAndTypes.insert(info.name, info.type);
+ }
+ continue;
+ }
+
+ // Case 2: Isolated identifier. Can only expand to a module name.
+ // Example: "Q^" -> "Qt.core", "Qt.widgets", ...
+ // Case 3: Prefixes match a module prefix. Identifier can only expand to a module name.
+ // Example: "Qt.c^" -> "Qt.core", "Qt.concurrent", ...
+ QString fullPrefix = identifierPrefix;
+ int nameOffset = 0;
+ if (!modulePrefixes.isEmpty()) {
+ fullPrefix.prepend(moduleNameOrPrefix + '.');
+ nameOffset = moduleNameOrPrefix.length() + 1;
+ }
+ for (const QString &module : allModules) {
+ if (module.startsWith(fullPrefix))
+ namesAndTypes.insert(module.mid(nameOffset), {});
+ }
+ }
+
+ for (auto it = namesAndTypes.cbegin(); it != namesAndTypes.cend(); ++it) {
+ lsp::CompletionItem item;
+ item.setLabel(it.key());
+ if (!it.value().isEmpty())
+ item.setDetail(it.value());
+ results.append(QJsonObject(item));
+ };
+ sendResponse(results);
+}
+
void LspServer::Private::handleShutdownRequest()
{
state = State::Shutdown;
@@ -425,14 +602,13 @@ void LspServer::Private::handleDidCloseNotification()
static int posToOffset(const CodePosition &pos, const QString &doc)
{
int offset = 0;
- int next = 0;
- for (int newlines = 0; newlines < pos.line() - 1; ++newlines) {
+ for (int newlines = 0, next = 0; newlines < pos.line() - 1; ++newlines) {
offset = doc.indexOf('\n', next);
if (offset == -1)
return -1;
next = offset + 1;
}
- return offset + pos.column() - 1;
+ return offset + pos.column();
}
bool Document::isPositionUpToDate(const CodePosition &pos) const
diff --git a/src/app/qbs/lspserver.h b/src/app/qbs/lspserver.h
index 2538789ca..566808309 100644
--- a/src/app/qbs/lspserver.h
+++ b/src/app/qbs/lspserver.h
@@ -43,7 +43,9 @@
#include <QString>
-namespace qbs::Internal {
+namespace qbs {
+class ProjectData;
+namespace Internal {
class LspServer
{
@@ -51,7 +53,7 @@ public:
LspServer();
~LspServer();
- void updateProjectData(const CodeLinks &codeLinks);
+ void updateProjectData(const ProjectData &projectData, const CodeLinks &codeLinks);
QString socketPath() const;
private:
@@ -59,4 +61,5 @@ private:
Private * const d;
};
-} // namespace qbs::Internal
+} // namespace Internal
+} // namespace qbs
diff --git a/src/app/qbs/session.cpp b/src/app/qbs/session.cpp
index d69a11b24..ebc9015b2 100644
--- a/src/app/qbs/session.cpp
+++ b/src/app/qbs/session.cpp
@@ -288,7 +288,7 @@ void Session::setupProject(const QJsonObject &request)
const ProjectData oldProjectData = m_projectData;
m_project = setupJob->project();
m_projectData = m_project.projectData();
- m_lspServer.updateProjectData(m_project.codeLinks());
+ m_lspServer.updateProjectData(m_projectData, m_project.codeLinks());
QJsonObject reply;
reply.insert(StringConstants::type(), QLatin1String("project-resolved"));
if (success)
diff --git a/src/lib/corelib/api/projectdata.cpp b/src/lib/corelib/api/projectdata.cpp
index 11469ee18..34e679d6d 100644
--- a/src/lib/corelib/api/projectdata.cpp
+++ b/src/lib/corelib/api/projectdata.cpp
@@ -40,6 +40,7 @@
#include "projectdata_p.h"
#include "propertymap_p.h"
+#include <language/builtindeclarations.h>
#include <language/language.h>
#include <language/propertymapinternal.h>
#include <loader/loaderutils.h>
@@ -49,7 +50,6 @@
#include <tools/qttools.h>
#include <tools/stlutils.h>
#include <tools/stringconstants.h>
-#include <tools/stringconstants.h>
#include <QtCore/qdir.h>
#include <QtCore/qjsonarray.h>
@@ -732,21 +732,16 @@ bool operator==(const ProductData &lhs, const ProductData &rhs)
if (!lhs.isValid() && !rhs.isValid())
return true;
- return lhs.isValid() == rhs.isValid()
- && lhs.name() == rhs.name()
- && lhs.targetName() == rhs.targetName()
- && lhs.type() == rhs.type()
- && lhs.version() == rhs.version()
- && lhs.dependencies() == rhs.dependencies()
- && lhs.profile() == rhs.profile()
- && lhs.multiplexConfigurationId() == rhs.multiplexConfigurationId()
- && lhs.location() == rhs.location()
- && lhs.groups() == rhs.groups()
- && lhs.generatedArtifacts() == rhs.generatedArtifacts()
- && lhs.properties() == rhs.properties()
- && lhs.moduleProperties() == rhs.moduleProperties()
- && lhs.isEnabled() == rhs.isEnabled()
- && lhs.isMultiplexed() == rhs.isMultiplexed();
+ return lhs.isValid() == rhs.isValid() && lhs.name() == rhs.name()
+ && lhs.targetName() == rhs.targetName() && lhs.type() == rhs.type()
+ && lhs.version() == rhs.version() && lhs.dependencies() == rhs.dependencies()
+ && lhs.profile() == rhs.profile()
+ && lhs.multiplexConfigurationId() == rhs.multiplexConfigurationId()
+ && lhs.location() == rhs.location() && lhs.groups() == rhs.groups()
+ && lhs.generatedArtifacts() == rhs.generatedArtifacts()
+ && qVariantMapsEqual(lhs.properties(), rhs.properties())
+ && lhs.moduleProperties() == rhs.moduleProperties() && lhs.isEnabled() == rhs.isEnabled()
+ && lhs.isMultiplexed() == rhs.isMultiplexed();
}
bool operator!=(const ProductData &lhs, const ProductData &rhs)
@@ -954,6 +949,52 @@ QStringList PropertyMap::allProperties() const
}
/*!
+ * \brief Returns the names of all modules whose properties can be requested.
+ */
+QStringList PropertyMap::allModules() const
+{
+ QStringList modules;
+ for (auto it = d->m_map->value().constBegin(); it != d->m_map->value().constEnd(); ++it) {
+ if (it.value().canConvert<QVariantMap>())
+ modules << it.key();
+ }
+ return modules;
+}
+
+/*!
+ * \brief Returns information about all properties of the given module.
+ */
+QList<PropertyMap::PropertyInfo> PropertyMap::allPropertiesForModule(const QString &module) const
+{
+ const QVariantMap moduleProps = d->m_map->value().value(module).toMap();
+ QList<PropertyInfo> properties;
+ const auto builtinProps = transformed<QStringList>(
+ BuiltinDeclarations::instance().declarationsForType(ItemType::Module).properties(),
+ [](const PropertyDeclaration &decl) { return decl.name(); });
+ for (auto it = moduleProps.begin(); it != moduleProps.end(); ++it) {
+ static const auto getType = [](const QVariant &v) -> QString {
+ switch (qVariantType(v)) {
+ case QMetaType::Bool:
+ return PropertyDeclaration::typeString(PropertyDeclaration::Boolean);
+ case QMetaType::Int:
+ return PropertyDeclaration::typeString(PropertyDeclaration::Integer);
+ case QMetaType::QVariantList:
+ return PropertyDeclaration::typeString(PropertyDeclaration::VariantList);
+ case QMetaType::QString:
+ return PropertyDeclaration::typeString(PropertyDeclaration::String);
+ case QMetaType::QStringList:
+ return PropertyDeclaration::typeString(PropertyDeclaration::StringList);
+ default:
+ return PropertyDeclaration::typeString(PropertyDeclaration::Variant);
+ }
+ };
+ properties << PropertyInfo{
+ it.key(), getType(it.value()), it.value(), builtinProps.contains(it.key())};
+ }
+ return properties;
+}
+
+/*!
* \brief Returns the value of the given property of a product or group.
*/
QVariant PropertyMap::getProperty(const QString &name) const
diff --git a/src/lib/corelib/api/projectdata.h b/src/lib/corelib/api/projectdata.h
index 9fe6445c7..4b7bc2803 100644
--- a/src/lib/corelib/api/projectdata.h
+++ b/src/lib/corelib/api/projectdata.h
@@ -74,6 +74,14 @@ class QBS_EXPORT PropertyMap
friend QBS_EXPORT bool operator!=(const PropertyMap &, const PropertyMap &);
public:
+ struct PropertyInfo
+ {
+ QString name;
+ QString type;
+ QVariant value;
+ bool isBuiltin = false;
+ };
+
PropertyMap();
PropertyMap(const PropertyMap &other);
PropertyMap(PropertyMap &&other) Q_DECL_NOEXCEPT;
@@ -83,6 +91,8 @@ public:
PropertyMap &operator =(PropertyMap &&other) Q_DECL_NOEXCEPT;
QStringList allProperties() const;
+ QStringList allModules() const;
+ QList<PropertyInfo> allPropertiesForModule(const QString &module) const;
QVariant getProperty(const QString &name) const;
QStringList getModulePropertiesAsStringList(const QString &moduleName,
diff --git a/src/lib/corelib/buildgraph/buildgraph.cpp b/src/lib/corelib/buildgraph/buildgraph.cpp
index d641627e7..10aae5991 100644
--- a/src/lib/corelib/buildgraph/buildgraph.cpp
+++ b/src/lib/corelib/buildgraph/buildgraph.cpp
@@ -710,7 +710,7 @@ void updateArtifactFromSourceArtifact(const ResolvedProductPtr &product,
const QVariantMap oldModuleProperties = artifact->properties->value();
setArtifactData(artifact, sourceArtifact);
if (oldFileTags != artifact->fileTags()
- || oldModuleProperties != artifact->properties->value()) {
+ || !qVariantMapsEqual(oldModuleProperties, artifact->properties->value())) {
invalidateArtifactAsRuleInputIfNecessary(artifact);
}
}
@@ -766,7 +766,7 @@ void updateGeneratedArtifacts(ResolvedProduct *product)
provideFullFileTagsAndProperties(artifact);
applyPerArtifactProperties(artifact);
if (oldFileTags != artifact->fileTags()
- || oldModuleProperties != artifact->properties->value()) {
+ || !qVariantMapsEqual(oldModuleProperties, artifact->properties->value())) {
invalidateArtifactAsRuleInputIfNecessary(artifact);
}
}
diff --git a/src/lib/corelib/buildgraph/buildgraphloader.cpp b/src/lib/corelib/buildgraph/buildgraphloader.cpp
index 023931e5c..6488ce9fd 100644
--- a/src/lib/corelib/buildgraph/buildgraphloader.cpp
+++ b/src/lib/corelib/buildgraph/buildgraphloader.cpp
@@ -755,10 +755,11 @@ bool BuildGraphLoader::checkProductForInstallInfoChanges(const ResolvedProductPt
<< StringConstants::installDirProperty() << StringConstants::installPrefixProperty()
<< StringConstants::installRootProperty();
for (const QString &key : specialProperties) {
- if (restoredProduct->moduleProperties->qbsPropertyValue(key)
- != newlyResolvedProduct->moduleProperties->qbsPropertyValue(key)) {
+ if (!qVariantsEqual(
+ restoredProduct->moduleProperties->qbsPropertyValue(key),
+ newlyResolvedProduct->moduleProperties->qbsPropertyValue(key))) {
qCDebug(lcBuildGraph).noquote().nospace()
- << "Product property 'qbs." << key << "' changed.";
+ << "Product property 'qbs." << key << "' changed.";
return true;
}
}
@@ -850,7 +851,8 @@ bool BuildGraphLoader::checkConfigCompatibility()
}
if (!m_parameters.overrideBuildGraphData()) {
if (!m_parameters.overriddenValues().empty()
- && m_parameters.overriddenValues() != restoredProject->overriddenValues) {
+ && !qVariantMapsEqual(
+ m_parameters.overriddenValues(), restoredProject->overriddenValues)) {
const auto toUserOutput = [](const QVariantMap &propMap) {
QString o;
for (auto it = propMap.begin(); it != propMap.end(); ++it) {
@@ -868,7 +870,7 @@ bool BuildGraphLoader::checkConfigCompatibility()
"you really want to rebuild with the new properties.")
.arg(toUserOutput(restoredProject->overriddenValues),
toUserOutput(m_parameters.overriddenValues())));
- }
+ }
m_parameters.setOverriddenValues(restoredProject->overriddenValues);
if (m_parameters.topLevelProfile() != restoredProject->profile()) {
throw ErrorInfo(Tr::tr("The current profile is '%1', but profile '%2' was used "
@@ -883,7 +885,8 @@ bool BuildGraphLoader::checkConfigCompatibility()
}
if (!m_parameters.overrideBuildGraphData())
return true;
- if (m_parameters.finalBuildConfigurationTree() != restoredProject->buildConfiguration())
+ if (!qVariantMapsEqual(
+ m_parameters.finalBuildConfigurationTree(), restoredProject->buildConfiguration()))
return false;
Settings settings(m_parameters.settingsDirectory());
const QVariantMap profileConfigsTree = restoredProject->fullProfileConfigsTree();
diff --git a/src/lib/corelib/buildgraph/rulecommands.cpp b/src/lib/corelib/buildgraph/rulecommands.cpp
index 6acd1d68c..73d05eaca 100644
--- a/src/lib/corelib/buildgraph/rulecommands.cpp
+++ b/src/lib/corelib/buildgraph/rulecommands.cpp
@@ -99,15 +99,11 @@ AbstractCommand::~AbstractCommand() = default;
bool AbstractCommand::equals(const AbstractCommand *other) const
{
- return type() == other->type()
- && m_description == other->m_description
- && m_extendedDescription == other->m_extendedDescription
- && m_highlight == other->m_highlight
- && m_ignoreDryRun == other->m_ignoreDryRun
- && m_silent == other->m_silent
- && m_jobPool == other->m_jobPool
- && m_timeout == other->m_timeout
- && m_properties == other->m_properties;
+ return type() == other->type() && m_description == other->m_description
+ && m_extendedDescription == other->m_extendedDescription
+ && m_highlight == other->m_highlight && m_ignoreDryRun == other->m_ignoreDryRun
+ && m_silent == other->m_silent && m_jobPool == other->m_jobPool
+ && m_timeout == other->m_timeout && qVariantMapsEqual(m_properties, other->m_properties);
}
void AbstractCommand::fillFromScriptValue(JSContext *ctx, const JSValue *scriptValue,
diff --git a/src/lib/corelib/buildgraph/rulesapplicator.cpp b/src/lib/corelib/buildgraph/rulesapplicator.cpp
index 5cc4be96e..e07abd74a 100644
--- a/src/lib/corelib/buildgraph/rulesapplicator.cpp
+++ b/src/lib/corelib/buildgraph/rulesapplicator.cpp
@@ -293,8 +293,9 @@ void RulesApplicator::doApply(const ArtifactSet &inputArtifacts, JSValue prepare
}
outputArtifact->properties->setValue(artifactModulesCfg);
if (!outputInfo.newlyCreated
- && (outputArtifact->fileTags() != outputInfo.oldFileTags
- || outputArtifact->properties->value() != outputInfo.oldProperties)) {
+ && (outputArtifact->fileTags() != outputInfo.oldFileTags
+ || !qVariantMapsEqual(
+ outputArtifact->properties->value(), outputInfo.oldProperties))) {
invalidateArtifactAsRuleInputIfNecessary(outputArtifact);
}
}
@@ -664,8 +665,10 @@ Artifact *RulesApplicator::createOutputArtifactFromScriptValue(const JSValue &ob
connect(outputInfo.artifact, dependency);
}
ArtifactBindingsExtractor().apply(engine(), outputInfo.artifact, obj);
- if (!outputInfo.newlyCreated && (outputInfo.artifact->fileTags() != outputInfo.oldFileTags
- || outputInfo.artifact->properties->value() != outputInfo.oldProperties)) {
+ if (!outputInfo.newlyCreated
+ && (outputInfo.artifact->fileTags() != outputInfo.oldFileTags
+ || !qVariantMapsEqual(
+ outputInfo.artifact->properties->value(), outputInfo.oldProperties))) {
invalidateArtifactAsRuleInputIfNecessary(outputInfo.artifact);
}
return outputInfo.artifact;
diff --git a/src/lib/corelib/buildgraph/transformerchangetracking.cpp b/src/lib/corelib/buildgraph/transformerchangetracking.cpp
index f0b8986f4..ae43e8219 100644
--- a/src/lib/corelib/buildgraph/transformerchangetracking.cpp
+++ b/src/lib/corelib/buildgraph/transformerchangetracking.cpp
@@ -158,7 +158,7 @@ bool TrafoChangeTracker::checkForPropertyChange(const Property &restoredProperty
case Property::PropertyInArtifact:
QBS_CHECK(false);
}
- if (restoredProperty.value != v) {
+ if (!qVariantsEqual(restoredProperty.value, v)) {
qCDebug(lcBuildGraph).noquote().nospace()
<< "Value for property '" << restoredProperty.moduleName << "."
<< restoredProperty.propertyName << "' has changed.\n"
diff --git a/src/lib/corelib/language/language.cpp b/src/lib/corelib/language/language.cpp
index d2c213999..1f491814a 100644
--- a/src/lib/corelib/language/language.cpp
+++ b/src/lib/corelib/language/language.cpp
@@ -939,7 +939,7 @@ bool operator==(const ExportedProperty &p1, const ExportedProperty &p2)
bool operator==(const ExportedModuleDependency &d1, const ExportedModuleDependency &d2)
{
- return d1.name == d2.name && d1.moduleProperties == d2.moduleProperties;
+ return d1.name == d2.name && qVariantMapsEqual(d1.moduleProperties, d2.moduleProperties);
}
bool equals(const std::vector<ExportedItemPtr> &l1, const std::vector<ExportedItemPtr> &l2)
@@ -966,20 +966,19 @@ bool operator==(const ExportedModule &m1, const ExportedModule &m2)
for (auto it1 = m1.cbegin(), it2 = m2.cbegin(); it1 != m1.cend(); ++it1, ++it2) {
if (it1.key()->name != it2.key()->name)
return false;
- if (it1.value() != it2.value())
+ if (!qVariantMapsEqual(it1.value(), it2.value()))
return false;
}
return true;
};
- return m1.propertyValues == m2.propertyValues
- && m1.modulePropertyValues == m2.modulePropertyValues
- && equals(m1.children, m2.children)
- && m1.m_properties == m2.m_properties
- && m1.importStatements == m2.importStatements
- && m1.productDependencies.size() == m2.productDependencies.size()
- && m1.productDependencies == m2.productDependencies
- && depMapsEqual(m1.dependencyParameters, m2.dependencyParameters);
+ return qVariantMapsEqual(m1.propertyValues, m2.propertyValues)
+ && qVariantMapsEqual(m1.modulePropertyValues, m2.modulePropertyValues)
+ && equals(m1.children, m2.children) && m1.m_properties == m2.m_properties
+ && m1.importStatements == m2.importStatements
+ && m1.productDependencies.size() == m2.productDependencies.size()
+ && m1.productDependencies == m2.productDependencies
+ && depMapsEqual(m1.dependencyParameters, m2.dependencyParameters);
}
JSValue PrivateScriptFunction::getFunction(ScriptEngine *engine, const QString &errorMessage) const
diff --git a/src/lib/corelib/language/propertydeclaration.cpp b/src/lib/corelib/language/propertydeclaration.cpp
index 9b1b890f4..d56ab3bb0 100644
--- a/src/lib/corelib/language/propertydeclaration.cpp
+++ b/src/lib/corelib/language/propertydeclaration.cpp
@@ -308,6 +308,26 @@ QVariant PropertyDeclaration::convertToPropertyType(const QVariant &v, Type t,
return c;
}
+QVariant PropertyDeclaration::typedNullValue() const
+{
+ switch (type()) {
+ case PropertyDeclaration::Boolean:
+ return typedNullVariant<bool>();
+ case PropertyDeclaration::Integer:
+ return typedNullVariant<int>();
+ case PropertyDeclaration::VariantList:
+ return typedNullVariant<QVariantList>();
+ case PropertyDeclaration::String:
+ case PropertyDeclaration::Path:
+ return typedNullVariant<QString>();
+ case PropertyDeclaration::StringList:
+ case PropertyDeclaration::PathList:
+ return typedNullVariant<QStringList>();
+ default:
+ return {};
+ }
+}
+
bool PropertyDeclaration::shouldCheckAllowedValues() const
{
return isValid()
diff --git a/src/lib/corelib/language/propertydeclaration.h b/src/lib/corelib/language/propertydeclaration.h
index a64094af8..79a39ecbd 100644
--- a/src/lib/corelib/language/propertydeclaration.h
+++ b/src/lib/corelib/language/propertydeclaration.h
@@ -128,6 +128,7 @@ public:
static QVariant convertToPropertyType(
const QVariant &v, Type t, const QStringList &namePrefix, const QString &key);
+ QVariant typedNullValue() const;
bool shouldCheckAllowedValues() const;
void checkAllowedValues(
diff --git a/src/lib/corelib/language/propertymapinternal.h b/src/lib/corelib/language/propertymapinternal.h
index 83e18ba48..af551cf6f 100644
--- a/src/lib/corelib/language/propertymapinternal.h
+++ b/src/lib/corelib/language/propertymapinternal.h
@@ -77,7 +77,7 @@ private:
inline bool operator==(const PropertyMapInternal &lhs, const PropertyMapInternal &rhs)
{
- return lhs.m_value == rhs.m_value;
+ return qVariantsEqual(lhs.m_value, rhs.m_value);
}
QVariant QBS_AUTOTEST_EXPORT moduleProperty(const QVariantMap &properties,
diff --git a/src/lib/corelib/language/scriptengine.cpp b/src/lib/corelib/language/scriptengine.cpp
index d655c0073..7847cb24d 100644
--- a/src/lib/corelib/language/scriptengine.cpp
+++ b/src/lib/corelib/language/scriptengine.cpp
@@ -488,6 +488,8 @@ void ScriptEngine::addInternalExtension(const char *name, JSValue ext)
JSValue ScriptEngine::asJsValue(const QVariant &v, quintptr id, bool frozen)
{
+ if (v.isNull())
+ return JS_UNDEFINED;
switch (static_cast<QMetaType::Type>(v.userType())) {
case QMetaType::QByteArray:
return asJsValue(v.toByteArray());
diff --git a/src/lib/corelib/loader/dependenciesresolver.cpp b/src/lib/corelib/loader/dependenciesresolver.cpp
index e49af1600..5df47217f 100644
--- a/src/lib/corelib/loader/dependenciesresolver.cpp
+++ b/src/lib/corelib/loader/dependenciesresolver.cpp
@@ -751,7 +751,7 @@ void DependenciesResolver::adjustDependsItemForMultiplexing(Item *dependsItem)
for (auto lhsProperty = lhs.constBegin(); lhsProperty != lhs.constEnd(); lhsProperty++) {
const auto rhsProperty = rhs.find(lhsProperty.key());
const bool isCommonProperty = rhsProperty != rhs.constEnd();
- if (isCommonProperty && lhsProperty.value() != rhsProperty.value())
+ if (isCommonProperty && !qVariantsEqual(lhsProperty.value(), rhsProperty.value()))
return false;
}
return true;
diff --git a/src/lib/corelib/loader/loaderutils.cpp b/src/lib/corelib/loader/loaderutils.cpp
index a78b3b4c5..a6105ee50 100644
--- a/src/lib/corelib/loader/loaderutils.cpp
+++ b/src/lib/corelib/loader/loaderutils.cpp
@@ -943,7 +943,7 @@ void DependencyParametersMerger::merge(QVariantMap &current, const QVariantMap &
currentValue = mdst;
} else {
if (m_currentPrio == nextPrio) {
- if (currentValue.isValid() && currentValue != newValue)
+ if (currentValue.isValid() && !qVariantsEqual(currentValue, newValue))
m_conflicts.emplace_back(m_path, currentValue, newValue, m_currentPrio);
} else {
removeIf(m_conflicts, [this](const Conflict &conflict) {
diff --git a/src/lib/corelib/loader/probesresolver.cpp b/src/lib/corelib/loader/probesresolver.cpp
index b38366900..efca2854c 100644
--- a/src/lib/corelib/loader/probesresolver.cpp
+++ b/src/lib/corelib/loader/probesresolver.cpp
@@ -212,7 +212,7 @@ void ProbesResolver::resolveProbe(ProductContext &productContext, Item *parent,
newValue = initialProperties.value(b.first);
}
}
- if (newValue != getJsVariant(ctx, b.second)) {
+ if (!qVariantsEqual(newValue, getJsVariant(ctx, b.second))) {
if (!resolvedProbe)
storedValue = VariantValue::createStored(newValue);
else
@@ -281,10 +281,10 @@ bool ProbesResolver::probeMatches(const ProbeConstPtr &probe, bool condition,
CompareScript compareScript) const
{
return probe->condition() == condition
- && probe->initialProperties() == initialProperties
- && (compareScript == CompareScript::No
- || (probe->configureScript() == configureScript
- && !probe->needsReconfigure(m_loaderState.topLevelProject().lastResolveTime())));
+ && qVariantMapsEqual(probe->initialProperties(), initialProperties)
+ && (compareScript == CompareScript::No
+ || (probe->configureScript() == configureScript
+ && !probe->needsReconfigure(m_loaderState.topLevelProject().lastResolveTime())));
}
} // namespace Internal
diff --git a/src/lib/corelib/loader/productresolver.cpp b/src/lib/corelib/loader/productresolver.cpp
index 9944fc56f..fd2f044a7 100644
--- a/src/lib/corelib/loader/productresolver.cpp
+++ b/src/lib/corelib/loader/productresolver.cpp
@@ -1589,7 +1589,13 @@ void PropertiesEvaluator::evaluateProperty(
} else if (pd.type() == PropertyDeclaration::VariantList) {
v = v.toList();
}
+
+ // Enforce proper type for undefined values (note that path degrades to string).
+ if (!v.isValid())
+ v = pd.typedNullValue();
+
pd.checkAllowedValues(v, propValue->location(), propName, m_loaderState);
+
result[propName] = v;
break;
}
diff --git a/src/lib/corelib/parser/qmljsastvisitor_p.h b/src/lib/corelib/parser/qmljsastvisitor_p.h
index bec174c65..4b7911aa4 100644
--- a/src/lib/corelib/parser/qmljsastvisitor_p.h
+++ b/src/lib/corelib/parser/qmljsastvisitor_p.h
@@ -58,7 +58,7 @@
namespace QbsQmlJS {
namespace AST {
-class QBS_AUTOTEST_EXPORT Visitor
+class QML_PARSER_EXPORT Visitor
{
public:
Visitor();
diff --git a/src/lib/corelib/parser/qmljsengine_p.h b/src/lib/corelib/parser/qmljsengine_p.h
index 2a616126d..9c603ee5c 100644
--- a/src/lib/corelib/parser/qmljsengine_p.h
+++ b/src/lib/corelib/parser/qmljsengine_p.h
@@ -93,7 +93,7 @@ public:
QString message;
};
-class QBS_AUTOTEST_EXPORT Engine
+class QML_PARSER_EXPORT Engine
{
Lexer *_lexer{nullptr};
Directives *_directives{nullptr};
diff --git a/src/lib/corelib/parser/qmljsglobal_p.h b/src/lib/corelib/parser/qmljsglobal_p.h
index c3d198ea5..02e0b38e4 100644
--- a/src/lib/corelib/parser/qmljsglobal_p.h
+++ b/src/lib/corelib/parser/qmljsglobal_p.h
@@ -41,31 +41,14 @@
#include <QtCore/qglobal.h>
-// Force QML_PARSER_EXPORT to be always empty.
-#ifndef QT_CREATOR
-# define QT_CREATOR
-#endif
-#ifdef QML_BUILD_STATIC_LIB
-# undef QML_BUILD_STATIC_LIB
-#endif
-#define QML_BUILD_STATIC_LIB 1
-
-#ifdef QT_CREATOR
-# ifdef QMLJS_BUILD_DIR
-# define QML_PARSER_EXPORT Q_DECL_EXPORT
-# elif QML_BUILD_STATIC_LIB
-# define QML_PARSER_EXPORT
-# else
-# define QML_PARSER_EXPORT Q_DECL_IMPORT
-# endif // QMLJS_BUILD_DIR
-
-#else // !QT_CREATOR
-# if defined(QT_BUILD_QMLDEVTOOLS_LIB) || defined(QT_QMLDEVTOOLS_LIB)
- // QmlDevTools is a static library
-# define QML_PARSER_EXPORT
-# else
-# define QML_PARSER_EXPORT Q_AUTOTEST_EXPORT
-# endif
-#endif // QT_CREATOR
+#ifdef QBS_STATIC_LIB
+#define QML_PARSER_EXPORT
+#else
+#ifdef QBS_LIBRARY
+#define QML_PARSER_EXPORT Q_DECL_EXPORT
+#else
+#define QML_PARSER_EXPORT Q_DECL_IMPORT
+#endif // QBS_LIBRARY
+#endif // QBS_STATIC_LIB
#endif // QMLJSGLOBAL_P_H
diff --git a/src/lib/corelib/parser/qmljslexer_p.h b/src/lib/corelib/parser/qmljslexer_p.h
index aef68e0c5..c9801c0f5 100644
--- a/src/lib/corelib/parser/qmljslexer_p.h
+++ b/src/lib/corelib/parser/qmljslexer_p.h
@@ -86,7 +86,7 @@ public:
}
};
-class QBS_AUTOTEST_EXPORT Lexer: public QmlJSGrammar
+class QML_PARSER_EXPORT Lexer : public QmlJSGrammar
{
public:
enum {
diff --git a/src/lib/corelib/parser/qmljsparser_p.h b/src/lib/corelib/parser/qmljsparser_p.h
index c761bb25b..9744a7eb6 100644
--- a/src/lib/corelib/parser/qmljsparser_p.h
+++ b/src/lib/corelib/parser/qmljsparser_p.h
@@ -70,7 +70,7 @@ namespace QbsQmlJS {
class Engine;
-class QBS_AUTOTEST_EXPORT Parser: protected QmlJSGrammar
+class QML_PARSER_EXPORT Parser : protected QmlJSGrammar
{
public:
union Value {
diff --git a/src/lib/corelib/tools/persistence.cpp b/src/lib/corelib/tools/persistence.cpp
index 44090dee2..0e545377a 100644
--- a/src/lib/corelib/tools/persistence.cpp
+++ b/src/lib/corelib/tools/persistence.cpp
@@ -140,6 +140,11 @@ void PersistentPool::finalizeWriteStream()
void PersistentPool::storeVariant(const QVariant &variant)
{
+ if (variant.isNull()) {
+ m_stream << quint32(QMetaType::User);
+ m_stream << variant;
+ return;
+ }
const auto type = static_cast<quint32>(variant.userType());
m_stream << type;
switch (type) {
@@ -231,8 +236,5 @@ void PersistentPool::doStoreValue(const QProcessEnvironment &env)
store(env.value(key));
}
-const PersistentPool::PersistentObjectId PersistentPool::ValueNotFoundId;
-const PersistentPool::PersistentObjectId PersistentPool::EmptyValueId;
-
} // namespace Internal
} // namespace qbs
diff --git a/src/lib/corelib/tools/persistence.h b/src/lib/corelib/tools/persistence.h
index b7aa543a4..86365c993 100644
--- a/src/lib/corelib/tools/persistence.h
+++ b/src/lib/corelib/tools/persistence.h
@@ -145,8 +145,9 @@ private:
template<typename T> QHash<T, PersistentObjectId> &idMap();
template<typename T> PersistentObjectId &lastStoredId();
- static const PersistentObjectId ValueNotFoundId = -1;
- static const PersistentObjectId EmptyValueId = -2;
+ static const inline PersistentObjectId ValueNotFoundId = -1;
+ static const inline PersistentObjectId EmptyValueId = -2;
+ static const inline PersistentObjectId NullValueId = -3;
std::unique_ptr<QIODevice> m_file;
QDataStream m_stream;
@@ -271,8 +272,13 @@ template<typename T> inline T PersistentPool::idLoadValue()
{
int id;
m_stream >> id;
- if (id == EmptyValueId)
+ if (id == NullValueId)
return T();
+ if (id == EmptyValueId) {
+ if constexpr (std::is_same_v<T, QString>)
+ return QString(0, QChar());
+ return T();
+ }
QBS_CHECK(id >= 0);
if (id >= static_cast<int>(idStorage<T>().size())) {
T value;
@@ -287,6 +293,12 @@ template<typename T> inline T PersistentPool::idLoadValue()
template<typename T>
void PersistentPool::idStoreValue(const T &value)
{
+ if constexpr (std::is_same_v<T, QString>) {
+ if (value.isNull()) {
+ m_stream << NullValueId;
+ return;
+ }
+ }
if (value.isEmpty()) {
m_stream << EmptyValueId;
return;
diff --git a/src/lib/corelib/tools/qttools.h b/src/lib/corelib/tools/qttools.h
index 88ada73d4..029948be4 100644
--- a/src/lib/corelib/tools/qttools.h
+++ b/src/lib/corelib/tools/qttools.h
@@ -199,6 +199,51 @@ inline bool qVariantConvert(QVariant &variant, int typeId)
#endif
}
+inline QMetaType::Type qVariantType(const QVariant &v)
+{
+ return static_cast<QMetaType::Type>(
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ v.metaType().id()
+#else
+ v.type()
+#endif
+ );
+}
+
+template<typename T>
+inline QVariant typedNullVariant()
+{
+ const auto metaType = QMetaType::fromType<T>();
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ return QVariant(metaType, nullptr);
+#else
+ return QVariant(static_cast<QVariant::Type>(metaType.id()));
+#endif
+}
+
+inline bool qVariantsEqual(const QVariant &v1, const QVariant &v2)
+{
+ return v1.isNull() == v2.isNull() && v1 == v2;
+}
+
+inline bool qVariantMapsEqual(const QVariantMap &m1, const QVariantMap &m2)
+{
+ if (m1.size() != m2.size())
+ return false;
+ if (m1.isSharedWith(m2))
+ return true;
+
+ auto it1 = m1.cbegin();
+ auto it2 = m2.cbegin();
+ while (it1 != m1.cend()) {
+ if (it1.key() != it2.key() || !qVariantsEqual(it1.value(), it2.value()))
+ return false;
+ ++it2;
+ ++it1;
+ }
+ return true;
+}
+
} // namespace qbs
#endif // QBSQTTOOLS_H
diff --git a/src/lib/corelib/tools/setupprojectparameters.cpp b/src/lib/corelib/tools/setupprojectparameters.cpp
index 28ad745ce..185323c09 100644
--- a/src/lib/corelib/tools/setupprojectparameters.cpp
+++ b/src/lib/corelib/tools/setupprojectparameters.cpp
@@ -47,6 +47,7 @@
#include <tools/jsonhelper.h>
#include <tools/profile.h>
#include <tools/qbsassert.h>
+#include <tools/qttools.h>
#include <tools/scripttools.h>
#include <tools/settings.h>
#include <tools/stringconstants.h>
@@ -517,7 +518,7 @@ ErrorInfo SetupProjectParameters::expandBuildConfiguration()
QVariantMap expandedConfig = expandedBuildConfiguration(profile, configurationName(), &err);
if (err.hasError())
return err;
- if (d->buildConfiguration != expandedConfig) {
+ if (!qVariantMapsEqual(d->buildConfiguration, expandedConfig)) {
d->buildConfigurationTree.clear();
d->buildConfiguration = expandedConfig;
}
diff --git a/tests/auto/api/tst_api.cpp b/tests/auto/api/tst_api.cpp
index d3141ce59..7726cee15 100644
--- a/tests/auto/api/tst_api.cpp
+++ b/tests/auto/api/tst_api.cpp
@@ -1925,7 +1925,8 @@ struct ProductDataSelector
bool qbsPropertiesMatch(const qbs::ProductData &p) const
{
for (auto it = qbsProperties.begin(); it != qbsProperties.end(); ++it) {
- if (it.value() != p.moduleProperties().getModuleProperty("qbs", it.key()))
+ if (!qbs::qVariantsEqual(
+ it.value(), p.moduleProperties().getModuleProperty("qbs", it.key())))
return false;
}
return true;
diff --git a/tests/auto/blackbox/testdata/lsp/lsp.qbs b/tests/auto/blackbox/testdata/lsp/lsp.qbs
index 2e30ad930..24479e0ec 100644
--- a/tests/auto/blackbox/testdata/lsp/lsp.qbs
+++ b/tests/auto/blackbox/testdata/lsp/lsp.qbs
@@ -3,6 +3,7 @@ Project {
name: "dep"
Depends { name: "m" }
Depends { name: "Prefix"; submodules: ["m1", "m2", "m3"] }
+
}
Product {
Depends { name: "dep" }
diff --git a/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs b/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs
index 84957060c..09bac2dc2 100644
--- a/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs
+++ b/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs
@@ -1,2 +1,5 @@
Module {
+ property bool p1
+ property string p2
+ property bool x
}
diff --git a/tests/auto/blackbox/tst_blackbox.cpp b/tests/auto/blackbox/tst_blackbox.cpp
index 11a4078ea..8ba6e2fa5 100644
--- a/tests/auto/blackbox/tst_blackbox.cpp
+++ b/tests/auto/blackbox/tst_blackbox.cpp
@@ -6369,20 +6369,33 @@ void TestBlackbox::qbsLanguageServer_data()
<< ((testDataDir + "/lsp/modules/Prefix/m1/m1.qbs:1:1\n")
+ (testDataDir + "/lsp/modules/Prefix/m2/m2.qbs:1:1\n")
+ (testDataDir + "/lsp/modules/Prefix/m3/m3.qbs:1:1"));
- QTest::addRow("follow to product") << "--goto-def"
- << (testDataDir + "/lsp/lsp.qbs:8:19")
- << QString() << QString()
- << (testDataDir + "/lsp/lsp.qbs:2:5");
+ QTest::addRow("follow to product")
+ << "--goto-def" << (testDataDir + "/lsp/lsp.qbs:9:19") << QString() << QString()
+ << (testDataDir + "/lsp/lsp.qbs:2:5");
QTest::addRow("follow to module, non-invalidating insert")
- << "--goto-def"
- << (testDataDir + "/lsp/lsp.qbs:4:9")
- << "5:9" << QString("property bool dummy\n")
- << (testDataDir + "/lsp/modules/m/m.qbs:1:1");
+ << "--goto-def" << (testDataDir + "/lsp/lsp.qbs:4:9") << "5:9"
+ << QString("property bool dummy\n") << (testDataDir + "/lsp/modules/m/m.qbs:1:1");
QTest::addRow("follow to module, invalidating insert")
- << "--goto-def"
- << (testDataDir + "/lsp/lsp.qbs:4:9")
- << QString() << QString("property bool dummy\n")
- << QString();
+ << "--goto-def" << (testDataDir + "/lsp/lsp.qbs:4:9") << QString()
+ << QString("property bool dummy\n") << QString();
+ QTest::addRow("completion: LHS, module prefix")
+ << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString() << QString("P")
+ << QString("Prefix.m1\nPrefix.m2\nPrefix.m3");
+ QTest::addRow("completion: LHS, module name")
+ << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString() << QString("Prefix.m")
+ << QString("m1\nm2\nm3");
+ QTest::addRow("completion: LHS, module property right after dot")
+ << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString()
+ << QString("Prefix.m1.") << QString("p1 bool\np2 string\nx bool");
+ QTest::addRow("completion: LHS, module property with identifier prefix")
+ << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString()
+ << QString("Prefix.m1.p") << QString("p1 bool\np2 string");
+ QTest::addRow("completion: simple RHS, module property")
+ << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString()
+ << QString("property bool dummy: Prefix.m1.p") << QString("p1 bool\np2 string");
+ QTest::addRow("completion: complex RHS, module property")
+ << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString()
+ << QString("property bool dummy: { return Prefix.m1.p") << QString("p1 bool\np2 string");
}
void TestBlackbox::qbsLanguageServer()
diff --git a/tests/auto/language/tst_language.cpp b/tests/auto/language/tst_language.cpp
index 7af4356cb..456e8b9d0 100644
--- a/tests/auto/language/tst_language.cpp
+++ b/tests/auto/language/tst_language.cpp
@@ -1928,7 +1928,7 @@ void TestLanguage::moduleParameters()
};
const QVariantMap actual = findInProduct(it.key());
const QVariantMap expected = it.value().toMap();
- const bool same = actual == expected;
+ const bool same = qVariantMapsEqual(actual, expected);
if (!same) {
qDebug().noquote() << "---" << expected;
qDebug().noquote() << "+++" << actual;
diff --git a/tests/lspclient/lspclient.cpp b/tests/lspclient/lspclient.cpp
index 8574161da..40adc9288 100644
--- a/tests/lspclient/lspclient.cpp
+++ b/tests/lspclient/lspclient.cpp
@@ -27,6 +27,7 @@
****************************************************************************/
#include <lsp/clientcapabilities.h>
+#include <lsp/completion.h>
#include <lsp/initializemessages.h>
#include <lsp/languagefeatures.h>
#include <lsp/textsynchronization.h>
@@ -40,7 +41,10 @@
#include <cstdlib>
#include <iostream>
-enum class Command { GotoDefinition, };
+enum class Command {
+ GotoDefinition,
+ Completion,
+};
class LspClient : public QObject
{
@@ -65,6 +69,8 @@ private:
void handleResponse();
void sendGotoDefinitionRequest();
void handleGotoDefinitionResponse();
+ void sendCompletionRequest();
+ void handleCompletionResponse();
lsp::DocumentUri uri() const;
lsp::DocumentUri::PathMapper mapper() const;
@@ -93,6 +99,8 @@ int main(int argc, char *argv[])
"socket");
const QCommandLineOption gotoDefinitionOption(
{"goto-def", "g"}, "Go to definition from the specified location.");
+ const QCommandLineOption completionOption(
+ {"completion", "c"}, "Request completion at the specified location.");
const QCommandLineOption insertCodeOption("insert-code",
"A piece of code to insert before doing the actual "
"operation.",
@@ -101,7 +109,12 @@ int main(int argc, char *argv[])
"The location at which to insert the code.",
"<line>:<column>");
QCommandLineParser parser;
- parser.addOptions({socketOption, insertCodeOption, insertLocationOption, gotoDefinitionOption});
+ parser.addOptions(
+ {socketOption,
+ insertCodeOption,
+ insertLocationOption,
+ gotoDefinitionOption,
+ completionOption});
parser.addHelpOption();
parser.addPositionalArgument("location", "The location at which to operate.",
"<file>:<line>:<column>");
@@ -120,6 +133,8 @@ int main(int argc, char *argv[])
if (parser.isSet(gotoDefinitionOption))
command = Command::GotoDefinition;
+ else if (parser.isSet(completionOption))
+ command = Command::Completion;
else
complainAndExit("Don't know what to do.");
@@ -339,6 +354,8 @@ void LspClient::sendRequest()
switch (m_command) {
case Command::GotoDefinition:
return sendGotoDefinitionRequest();
+ case Command::Completion:
+ return sendCompletionRequest();
}
}
@@ -352,6 +369,8 @@ void LspClient::handleResponse()
switch (m_command) {
case Command::GotoDefinition:
return handleGotoDefinitionResponse();
+ case Command::Completion:
+ return handleCompletionResponse();
}
}
@@ -380,6 +399,29 @@ void LspClient::handleGotoDefinitionResponse()
exit(EXIT_SUCCESS);
}
+void LspClient::sendCompletionRequest()
+{
+ const lsp::TextDocumentIdentifier doc(uri());
+ const lsp::Position pos(m_line - 1, m_column - 1);
+ sendMessage(lsp::CompletionRequest({doc, pos}));
+}
+
+void LspClient::handleCompletionResponse()
+{
+ const lsp::CompletionResult result(lsp::CompletionRequest::Response(m_messageObject)
+ .result()
+ .value_or(lsp::CompletionResult()));
+ if (const auto items = std::get_if<QList<lsp::CompletionItem>>(&result)) {
+ for (const lsp::CompletionItem &item : *items) {
+ std::cout << qPrintable(item.label());
+ if (item.detail())
+ std::cout << ' ' << qPrintable(*item.detail());
+ std::cout << std::endl;
+ }
+ }
+ exit(EXIT_SUCCESS);
+}
+
lsp::DocumentUri LspClient::uri() const
{
return lsp::DocumentUri::fromFilePath(lsp::Utils::FilePath::fromUserInput(m_filePath),