diff options
-rw-r--r-- | src/qmldom/qqmldomastcreator.cpp | 96 | ||||
-rw-r--r-- | src/qmldom/qqmldomelements.cpp | 10 | ||||
-rw-r--r-- | src/qmldom/qqmldomelements_p.h | 4 | ||||
-rw-r--r-- | src/qmldom/qqmldompath_p.h | 2 | ||||
-rw-r--r-- | src/qmlls/qqmllsutils.cpp | 57 | ||||
-rw-r--r-- | tests/auto/qmldom/domdata/domitem/objectBindings.qml | 13 | ||||
-rw-r--r-- | tests/auto/qmldom/domitem/tst_qmldomitem.h | 61 | ||||
-rw-r--r-- | tests/auto/qmlls/utils/tst_qmlls_utils.cpp | 31 |
8 files changed, 216 insertions, 58 deletions
diff --git a/src/qmldom/qqmldomastcreator.cpp b/src/qmldom/qqmldomastcreator.cpp index a748038671..76c9acc5f9 100644 --- a/src/qmldom/qqmldomastcreator.cpp +++ b/src/qmldom/qqmldomastcreator.cpp @@ -119,6 +119,46 @@ SourceLocation combineLocations(Node *n) return combineLocations(n->firstSourceLocation(), n->lastSourceLocation()); } +static ScriptElementVariant wrapIntoFieldMemberExpression(const ScriptElementVariant &left, + const SourceLocation &dotToken, + const ScriptElementVariant &right) +{ + SourceLocation s1, s2; + left.visitConst([&s1](auto &&el) { s1 = el->mainRegionLocation(); }); + right.visitConst([&s2](auto &&el) { s2 = el->mainRegionLocation(); }); + + auto result = std::make_shared<ScriptElements::BinaryExpression>(s1, s2); + result->addLocation(OperatorTokenRegion, dotToken); + result->setOp(ScriptElements::BinaryExpression::FieldMemberAccess); + result->setLeft(left); + result->setRight(right); + return ScriptElementVariant::fromElement(result); +}; + +/*! + \internal + Creates a FieldMemberExpression if the qualified id has dots. +*/ +static ScriptElementVariant fieldMemberExpressionForQualifiedId(AST::UiQualifiedId *qualifiedId) +{ + ScriptElementVariant bindable; + bool first = true; + for (auto exp = qualifiedId; exp; exp = exp->next) { + const SourceLocation identifierLoc = exp->identifierToken; + auto id = std::make_shared<ScriptElements::IdentifierExpression>(identifierLoc); + id->setName(exp->name); + if (first) { + first = false; + bindable = ScriptElementVariant::fromElement(id); + continue; + } + bindable = wrapIntoFieldMemberExpression(bindable, exp->dotToken, + ScriptElementVariant::fromElement(id)); + } + + return bindable; +} + QQmlDomAstCreator::QmlStackElement &QQmlDomAstCreator::currentQmlObjectOrComponentEl(int idx) { Q_ASSERT_X(idx < nodeStack.size() && idx >= 0, "currentQmlObjectOrComponentEl", @@ -751,6 +791,15 @@ bool QQmlDomAstCreator::visit(AST::UiObjectDefinition *el) el->qualifiedTypeNameId->identifierToken); } Q_ASSERT_X(sPtr, className, "could not recover new scope"); + + if (m_enableScriptExpressions) { + auto qmlObjectType = makeGenericScriptElement(el->qualifiedTypeNameId, DomType::ScriptType); + qmlObjectType->insertChild(Fields::typeName, + fieldMemberExpressionForQualifiedId(el->qualifiedTypeNameId)); + sPtr->setNameIdentifiers( + finalizeScriptExpression(ScriptElementVariant::fromElement(qmlObjectType), + sPathFromOwner.field(Fields::nameIdentifiers), rootMap)); + } pushEl(sPathFromOwner, *sPtr, el); loadAnnotations(el); return true; @@ -814,6 +863,16 @@ bool QQmlDomAstCreator::visit(AST::UiObjectBinding *el) QmlObject *objValue = bPtr->objectValue(); Q_ASSERT_X(objValue, className, "could not recover objectValue"); objValue->setName(toString(el->qualifiedTypeNameId)); + + if (m_enableScriptExpressions) { + auto qmlObjectType = makeGenericScriptElement(el->qualifiedTypeNameId, DomType::ScriptType); + qmlObjectType->insertChild(Fields::typeName, + fieldMemberExpressionForQualifiedId(el->qualifiedTypeNameId)); + objValue->setNameIdentifiers(finalizeScriptExpression( + ScriptElementVariant::fromElement(qmlObjectType), + bPathFromOwner.field(Fields::value).field(Fields::nameIdentifiers), rootMap)); + } + objValue->addPrototypePath(Paths::lookupTypePath(objValue->name())); pushEl(bPathFromOwner.field(Fields::value), *objValue, el->initializer); return true; @@ -889,22 +948,6 @@ bool QQmlDomAstCreator::visit(AST::UiScriptBinding *el) .withPath(pathFromOwner))); } } else { - // Create FieldExpression if the bindable element has dots - const auto reparentExp = [](const auto &left, const SourceLocation &dotToken, - const auto &right) { - SourceLocation s1, s2; - left.visitConst([&s1](auto &&el) { s1 = el->mainRegionLocation(); }); - - right.visitConst([&s2](auto &&el) { s2 = el->mainRegionLocation(); }); - - auto result = std::make_shared<ScriptElements::BinaryExpression>(s1, s2); - result->addLocation(OperatorTokenRegion, dotToken); - result->setOp(ScriptElements::BinaryExpression::FieldMemberAccess); - result->setLeft(left); - result->setRight(right); - return ScriptElementVariant::fromElement(result); - }; - pathFromOwner = current<QmlObject>().addBinding(bindingV, AddOption::KeepExisting, &bindingPtr); QmlStackElement &containingObjectEl = currentEl<QmlObject>(); @@ -916,19 +959,7 @@ bool QQmlDomAstCreator::visit(AST::UiScriptBinding *el) el->qualifiedId->identifierToken); FileLocations::addRegion(bindingFileLocation, ColonTokenRegion, el->colonToken); - ScriptElementVariant bindable; - bool first = true; - for (auto exp = el->qualifiedId; exp; exp = exp->next) { - const SourceLocation identifierLoc = exp->identifierToken; - auto id = std::make_shared<ScriptElements::IdentifierExpression>(identifierLoc); - id->setName(exp->name); - if (first) { - first = false; - bindable = ScriptElementVariant::fromElement(id); - continue; - } - bindable = reparentExp(bindable, exp->dotToken, ScriptElementVariant::fromElement(id)); - } + ScriptElementVariant bindable = fieldMemberExpressionForQualifiedId(el->qualifiedId); bindingPtr->setBindingIdentifiers(finalizeScriptExpression( bindable, pathFromOwner.field(Fields::bindingIdentifiers), rootMap)); @@ -1938,13 +1969,12 @@ void QQmlDomAstCreator::endVisit(AST::Type *exp) auto current = makeGenericScriptElement(exp, DomType::ScriptType); if (exp->typeArgument) { - auto currentChild = scriptElementForQualifiedId(exp->typeArgument); - current->insertChild(Fields::typeArgument, currentChild); + current->insertChild(Fields::typeArgumentName, + fieldMemberExpressionForQualifiedId(exp->typeArgument)); } if (exp->typeId) { - auto currentChild = scriptElementForQualifiedId(exp->typeId); - current->insertChild(Fields::typeName, currentChild); + current->insertChild(Fields::typeName, fieldMemberExpressionForQualifiedId(exp->typeId)); } pushScriptElement(current); diff --git a/src/qmldom/qqmldomelements.cpp b/src/qmldom/qqmldomelements.cpp index fe25087763..dd1cd0be7b 100644 --- a/src/qmldom/qqmldomelements.cpp +++ b/src/qmldom/qqmldomelements.cpp @@ -412,6 +412,11 @@ bool QmlObject::iterateBaseDirectSubpaths(const DomItem &self, DirectVisitor vis [&self](const DomItem &) { return self.propertyInfoNames(); }, QLatin1String("PropertyInfo"))); }); + if (m_nameIdentifiers) { + cont = cont && self.dvItemField(visitor, Fields::nameIdentifiers, [this, &self]() { + return self.subScriptElementWrapperItem(m_nameIdentifiers); + }); + } return cont; } @@ -505,6 +510,11 @@ DomItem QmlObject::field(const DomItem &self, QStringView name) const [copiedSelf = self](const DomItem &) { return copiedSelf.propertyInfoNames(); }, QLatin1String("PropertyInfo"))); break; + case 15: + if (name == Fields::nameIdentifiers && m_nameIdentifiers) { + return self.subScriptElementWrapperItem(m_nameIdentifiers); + } + break; case 19: if (name == Fields::defaultPropertyName) return self.subDataItem(PathEls::Field(Fields::defaultPropertyName), diff --git a/src/qmldom/qqmldomelements_p.h b/src/qmldom/qqmldomelements_p.h index e1afa0cff3..d71f97851c 100644 --- a/src/qmldom/qqmldomelements_p.h +++ b/src/qmldom/qqmldomelements_p.h @@ -914,6 +914,9 @@ public: QQmlJSScope::ConstPtr semanticScope() const { return m_scope; } void setSemanticScope(const QQmlJSScope::ConstPtr &scope) { m_scope = scope; } + ScriptElementVariant nameIdentifiers() const { return m_nameIdentifiers; } + void setNameIdentifiers(const ScriptElementVariant &name) { m_nameIdentifiers = name; } + private: friend class QQmlDomAstCreator; QString m_idStr; @@ -927,6 +930,7 @@ private: QList<QmlObject> m_children; QList<QmlObject> m_annotations; QQmlJSScope::ConstPtr m_scope; + ScriptElementVariant m_nameIdentifiers; }; class Export diff --git a/src/qmldom/qqmldompath_p.h b/src/qmldom/qqmldompath_p.h index be85ebd408..32ee25122f 100644 --- a/src/qmldom/qqmldompath_p.h +++ b/src/qmldom/qqmldompath_p.h @@ -502,6 +502,7 @@ QMLDOM_FIELD(nCallbacks); QMLDOM_FIELD(nLoaded); QMLDOM_FIELD(nNotdone); QMLDOM_FIELD(name); +QMLDOM_FIELD(nameIdentifiers); QMLDOM_FIELD(newlinesBefore); QMLDOM_FIELD(nextComponent); QMLDOM_FIELD(nextScope); @@ -568,6 +569,7 @@ QMLDOM_FIELD(targetPropertyName); QMLDOM_FIELD(text); QMLDOM_FIELD(type); QMLDOM_FIELD(typeArgument); +QMLDOM_FIELD(typeArgumentName); QMLDOM_FIELD(typeName); QMLDOM_FIELD(types); QMLDOM_FIELD(universe); diff --git a/src/qmlls/qqmllsutils.cpp b/src/qmlls/qqmllsutils.cpp index 99357e172c..8b636082e4 100644 --- a/src/qmlls/qqmllsutils.cpp +++ b/src/qmlls/qqmllsutils.cpp @@ -75,6 +75,42 @@ static bool isFieldMemberAccess(const DomItem &item) /*! \internal + Get the bits of a field member expression, like \c{a}, \c{b} and \c{c} for \c{a.b.c}. + + stopAtChild can either be an FieldMemberExpression, a ScriptIdentifierExpression or a default + constructed DomItem: This exits early before processing Field::right of an + FieldMemberExpression stopAtChild, or before processing a ScriptIdentifierExpression stopAtChild. + No early exits if stopAtChild is default constructed. +*/ +static QStringList fieldMemberExpressionBits(const DomItem &item, const DomItem &stopAtChild = {}) +{ + const bool isAccess = isFieldMemberAccess(item); + const bool isExpression = isFieldMemberExpression(item); + + // assume it is a non-qualified name + if (!isAccess && !isExpression) + return { item.value().toString() }; + + const DomItem stopMarker = + isFieldMemberExpression(stopAtChild) ? stopAtChild : stopAtChild.directParent(); + + QStringList result; + DomItem current = + isAccess ? item.directParent() : (isFieldMemberExpression(item) ? item : DomItem{}); + + for (; isFieldMemberExpression(current); current = current.field(Fields::right)) { + result << current.field(Fields::left).value().toString(); + + if (current == stopMarker) + return result; + } + result << current.value().toString(); + + return result; +} + +/*! + \internal The language server protocol calls "URI" what QML calls "URL". According to RFC 3986, a URL is a special case of URI that not only identifies a resource but also shows how to access it. @@ -352,10 +388,7 @@ QList<QQmlLSUtilsItemLocation> QQmlLSUtils::itemsFromTextLocation(const DomItem DomItem QQmlLSUtils::baseObject(const DomItem &object) { - if (!object.as<QmlObject>()) - return {}; - - auto prototypes = object.field(QQmlJS::Dom::Fields::prototypes); + auto prototypes = object.qmlObject().field(QQmlJS::Dom::Fields::prototypes); switch (prototypes.indexes()) { case 0: return {}; @@ -449,8 +482,15 @@ std::optional<QQmlLSUtilsLocation> QQmlLSUtils::findTypeDefinitionOf(const DomIt [](DomType k, const DomItem &) { return k == DomType::ScriptType; }, FilterUpOptions::ReturnOuter)) { - const QString name = type.field(Fields::typeName).value().toString(); - typeDefinition = object.path(Paths::lookupTypePath(name)); + const QString name = fieldMemberExpressionBits(type.field(Fields::typeName)).join(u'.'); + if (type.directParent().internalKind() == DomType::QmlObject) { + // is the type name of a QmlObject, like Item in `Item {...}` + typeDefinition = baseObject(type.directParent()); + } else { + // is a type annotation, like Item in `function f(x: Item) { ... }` + typeDefinition = object.path(Paths::lookupTypePath(name)); + } + break; } if (DomItem id = object.filterUp( @@ -1358,7 +1398,7 @@ DomItem QQmlLSUtils::sourceLocationToDomItem(const DomItem &file, static std::optional<QQmlLSUtilsLocation> findMethodDefinitionOf(const DomItem &file, QQmlJS::SourceLocation location, const QString &name) { - DomItem owner = QQmlLSUtils::sourceLocationToDomItem(file, location); + DomItem owner = QQmlLSUtils::sourceLocationToDomItem(file, location).qmlObject(); DomItem method = owner.field(Fields::methods).key(name).index(0); auto fileLocation = FileLocations::treeOf(method); if (!fileLocation) @@ -1380,7 +1420,8 @@ static std::optional<QQmlLSUtilsLocation> findPropertyDefinitionOf(const DomItem &file, QQmlJS::SourceLocation propertyDefinitionLocation, const QString &name) { - DomItem propertyOwner = QQmlLSUtils::sourceLocationToDomItem(file, propertyDefinitionLocation); + DomItem propertyOwner = + QQmlLSUtils::sourceLocationToDomItem(file, propertyDefinitionLocation).qmlObject(); DomItem propertyDefinition = propertyOwner.field(Fields::propertyDefs).key(name).index(0); auto fileLocation = FileLocations::treeOf(propertyDefinition); if (!fileLocation) diff --git a/tests/auto/qmldom/domdata/domitem/objectBindings.qml b/tests/auto/qmldom/domdata/domitem/objectBindings.qml new file mode 100644 index 0000000000..d47d60e5ea --- /dev/null +++ b/tests/auto/qmldom/domdata/domitem/objectBindings.qml @@ -0,0 +1,13 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick as QQ + +Item { + id: root + Item {} + QQ.Item {} + property var x: root. + QQ.Drag {} +} diff --git a/tests/auto/qmldom/domitem/tst_qmldomitem.h b/tests/auto/qmldom/domitem/tst_qmldomitem.h index 3fbe32c920..ead1db101f 100644 --- a/tests/auto/qmldom/domitem/tst_qmldomitem.h +++ b/tests/auto/qmldom/domitem/tst_qmldomitem.h @@ -2962,6 +2962,67 @@ private slots: u"a"_s); } + void objectBindings() + { + using namespace Qt::StringLiterals; + const QString testFile = baseDir + u"/objectBindings.qml"_s; + const DomItem rootQmlObject = rootQmlObjectFromFile(testFile, qmltypeDirs); + + const DomItem xBinding = rootQmlObject.path(".bindings[\"x\"][0].value"); + QCOMPARE(xBinding.field(Fields::name).value().toString(), u"root.QQ.Drag"); + QCOMPARE(xBinding.field(Fields::nameIdentifiers).internalKind(), + DomType::ScriptType); + QCOMPARE(xBinding.field(Fields::nameIdentifiers).field(Fields::typeName).internalKind(), + DomType::ScriptBinaryExpression); + QCOMPARE(xBinding.field(Fields::nameIdentifiers) + .field(Fields::typeName) + .field(Fields::operation) + .value() + .toInteger(-1), + ScriptElements::BinaryExpression::FieldMemberAccess); + + QCOMPARE(xBinding.field(Fields::nameIdentifiers).field(Fields::typeName).field(Fields::left).internalKind(), + DomType::ScriptBinaryExpression); + QCOMPARE(xBinding.field(Fields::nameIdentifiers) + .field(Fields::typeName) + .field(Fields::left) + .field(Fields::right) + .value() + .toString(), + u"QQ"); + QCOMPARE(xBinding.field(Fields::nameIdentifiers) + .field(Fields::typeName) + .field(Fields::left) + .field(Fields::left) + .value() + .toString(), + u"root"); + + const DomItem item = rootQmlObject.path(".children[0]"); + QCOMPARE(item.field(Fields::nameIdentifiers).field(Fields::typeName).value().toString(), + u"Item"); + + const DomItem qqItem = rootQmlObject.path(".children[1]"); + QCOMPARE(qqItem.field(Fields::nameIdentifiers) + .field(Fields::typeName) + .field(Fields::operation) + .value() + .toInteger(-1), + ScriptElements::BinaryExpression::FieldMemberAccess); + QCOMPARE(qqItem.field(Fields::nameIdentifiers) + .field(Fields::typeName) + .field(Fields::right) + .value() + .toString(), + u"Item"); + QCOMPARE(qqItem.field(Fields::nameIdentifiers) + .field(Fields::typeName) + .field(Fields::left) + .value() + .toString(), + u"QQ"); + } + private: QString baseDir; QStringList qmltypeDirs; diff --git a/tests/auto/qmlls/utils/tst_qmlls_utils.cpp b/tests/auto/qmlls/utils/tst_qmlls_utils.cpp index c70638bfa5..519bbafa83 100644 --- a/tests/auto/qmlls/utils/tst_qmlls_utils.cpp +++ b/tests/auto/qmlls/utils/tst_qmlls_utils.cpp @@ -189,7 +189,8 @@ void tst_qmlls_utils::findItemFromLocation_data() << QQmlJS::Dom::DomType::PropertyDefinition << -1 << 26; QTest::addRow("onCChild") << file1Qml << 16 << positionAfterOneIndent << firstResult << outOfOne - << QQmlJS::Dom::DomType::QmlObject << -1 << positionAfterOneIndent; + << QQmlJS::Dom::DomType::ScriptIdentifierExpression << -1 + << positionAfterOneIndent; // check for off-by-one/overlapping items QTest::addRow("closingBraceOfC") @@ -201,11 +202,12 @@ void tst_qmlls_utils::findItemFromLocation_data() QTest::addRow("firstBetweenCandD") << file1Qml << 16 << 20 << secondResult << outOfTwo << QQmlJS::Dom::DomType::QmlObject << -1 << positionAfterOneIndent; - QTest::addRow("secondBetweenCandD") << file1Qml << 16 << 20 << firstResult << outOfTwo - << QQmlJS::Dom::DomType::QmlObject << -1 << -1; + QTest::addRow("secondBetweenCandD") + << file1Qml << 16 << 20 << firstResult << outOfTwo + << QQmlJS::Dom::DomType::ScriptIdentifierExpression << -1 << -1; QTest::addRow("afterD") << file1Qml << 16 << 21 << firstResult << outOfOne - << QQmlJS::Dom::DomType::QmlObject << -1 << 20; + << QQmlJS::Dom::DomType::ScriptIdentifierExpression << -1 << 20; // check what happens between items (it should not crash) @@ -226,7 +228,7 @@ void tst_qmlls_utils::findItemFromLocation_data() QTest::addRow("ic") << file1Qml << 15 << 15 << firstResult << outOfOne << QQmlJS::Dom::DomType::QmlComponent << -1 << 5; QTest::addRow("ic2") << file1Qml << 15 << 20 << firstResult << outOfOne - << QQmlJS::Dom::DomType::QmlObject << -1 << 18; + << QQmlJS::Dom::DomType::ScriptIdentifierExpression << -1 << 18; QTest::addRow("ic3") << file1Qml << 15 << 33 << firstResult << outOfOne << QQmlJS::Dom::DomType::ScriptIdentifierExpression << -1 << 29; @@ -245,8 +247,9 @@ void tst_qmlls_utils::findItemFromLocation_data() << positionAfterOneIndent; // check rectangle property - QTest::addRow("rectangle-property") << file1Qml << 44 << 31 << firstResult << outOfOne - << QQmlJS::Dom::DomType::Binding << -1 << 24; + QTest::addRow("rectangle-property") + << file1Qml << 44 << 31 << firstResult << outOfOne + << QQmlJS::Dom::DomType::ScriptIdentifierExpression << -1 << 29; } void tst_qmlls_utils::findItemFromLocation() @@ -1846,9 +1849,9 @@ void tst_qmlls_utils::completions_data() QTest::newRow("asCompletions") << file << 26 << 9 << ExpectedCompletions({ - { u"Rectangle"_s, CompletionItemKind::Field }, + { u"Rectangle"_s, CompletionItemKind::Class }, }) - << QStringList({ u"foo"_s, u"import"_s, u"lala()"_s, u"width"_s }); + << QStringList({ u"foo"_s, u"import"_s, u"lala"_s, u"width"_s }); QTest::newRow("parameterCompletion") << file << 36 << 24 @@ -1906,7 +1909,7 @@ void tst_qmlls_utils::completions_data() }; QTest::newRow("propertyDefinitionBinding") - << file << 90 << 28 + << file << 90 << 27 << (ExpectedCompletions({ { u"lala"_s, CompletionItemKind::Method }, { u"createRectangle"_s, CompletionItemKind::Method }, @@ -1921,7 +1924,7 @@ void tst_qmlls_utils::completions_data() }; QTest::newRow("ignoreNonRelatedTypesForPropertyDefinitionBinding") - << file << 16 << 29 + << file << 16 << 28 << (ExpectedCompletions({ { u"createRectangle"_s, CompletionItemKind::Method }, { u"createItem"_s, CompletionItemKind::Method }, @@ -3276,12 +3279,6 @@ void tst_qmlls_utils::completions() const QString labelsForPrinting = sortedLabels.join(u", "_s); for (const ExpectedCompletion &exp : expected) { - QEXPECT_FAIL( - "asCompletions", - "Cannot complete after 'QQ.': either there is already a type behind and then " - "there is nothing to complete, or there is nothing behind 'QQ.' and the parser " - "fails because of the unexpected '.'", - Abort); QEXPECT_FAIL("letStatementAfterEqual", "Completion not implemented yet!", Abort); QEXPECT_FAIL("binaryExpressionMissingRHSWithDefaultProperty", "Current parser cannot recover from this error yet!", Abort); |