diff --git a/src/qmlcompiler/qqmljsimportvisitor.cpp b/src/qmlcompiler/qqmljsimportvisitor.cpp index b3de9f3808..528849854a 100644 --- a/src/qmlcompiler/qqmljsimportvisitor.cpp +++ b/src/qmlcompiler/qqmljsimportvisitor.cpp @@ -489,8 +489,13 @@ void QQmlJSImportVisitor::processDefaultProperties() // Assigning any element to a QQmlComponent property implicitly wraps it into a Component // Check whether the property can be assigned the scope - if (propType->canAssign(scope)) + if (propType->canAssign(scope)) { + if (propType->causesImplicitComponentWrapping()) { + // mark the scope as implicitly wrapped, unless it is a Component + scope->setIsWrappedInImplicitComponent(!scope->causesImplicitComponentWrapping()); + } continue; + } m_logger->logWarning( QStringLiteral("Cannot assign to default property of incompatible type"), @@ -529,6 +534,10 @@ void QQmlJSImportVisitor::processPropertyBindingObjects() if (property.isValid() && !property.type().isNull() && (objectBinding.onToken || property.type()->canAssign(objectBinding.childScope))) { + + if (property.type()->causesImplicitComponentWrapping()) + objectBinding.childScope->setIsWrappedInImplicitComponent(!objectBinding.childScope->causesImplicitComponentWrapping()); + QQmlJSMetaPropertyBinding binding = objectBinding.scope->hasOwnPropertyBinding(propertyName) ? objectBinding.scope->ownPropertyBinding(propertyName) @@ -629,7 +638,7 @@ void QQmlJSImportVisitor::checkRequiredProperties() } for (const auto &defScope : m_objectDefinitionScopes) { - if (defScope->parentScope() == m_globalScope || defScope->isInlineComponent()) + if (defScope->parentScope() == m_globalScope || defScope->isInlineComponent() || defScope->isComponentRootElement()) continue; QVector scopesToSearch; @@ -1611,7 +1620,8 @@ bool QQmlJSImportVisitor::visit(QQmlJS::AST::UiObjectBinding *uiob) void QQmlJSImportVisitor::endVisit(QQmlJS::AST::UiObjectBinding *uiob) { QQmlJSScope::resolveTypes(m_currentScope, m_rootScopeImports, &m_usedTypes); - const QQmlJSScope::ConstPtr childScope = m_currentScope; + // must be mutable, as we might mark it as implicitly wrapped in a component + const QQmlJSScope::Ptr childScope = m_currentScope; leaveEnvironment(); auto group = uiob->qualifiedId; diff --git a/src/qmlcompiler/qqmljsimportvisitor_p.h b/src/qmlcompiler/qqmljsimportvisitor_p.h index 13285186d8..c4b9e1ddf9 100644 --- a/src/qmlcompiler/qqmljsimportvisitor_p.h +++ b/src/qmlcompiler/qqmljsimportvisitor_p.h @@ -198,7 +198,7 @@ protected: struct PendingPropertyObjectBinding { QQmlJSScope::Ptr scope; - QQmlJSScope::ConstPtr childScope; + QQmlJSScope::Ptr childScope; QString name; QQmlJS::SourceLocation location; bool onToken; diff --git a/src/qmlcompiler/qqmljsscope.cpp b/src/qmlcompiler/qqmljsscope.cpp index ec951aff6f..7dc15e72e5 100644 --- a/src/qmlcompiler/qqmljsscope.cpp +++ b/src/qmlcompiler/qqmljsscope.cpp @@ -179,6 +179,45 @@ QQmlJSMetaEnum QQmlJSScope::enumeration(const QString &name) const return result; } +/*! + Returns if assigning to a property of this type would cause + implicit component wrapping for non-Component types. + + \note This method can also be used to check whether a type needs + to be implicitly wrapped: A type for which this function returns true + doesn't need to be actually wrapped. + */ +bool QQmlJSScope::causesImplicitComponentWrapping() const { + if (internalName() == u"QQmlComponent") + return true; + else if (isComposite()) // composite types are never treated as Component + return false; + // A class which is derived from component is not treated as a Component + // However isUsableComponent considers also QQmlAbstractDelegateComponent + // See isUsableComponent in qqmltypecompiler.cpp + + for (auto cppBase = nonCompositeBaseType(baseType()); cppBase; cppBase = cppBase->baseType()) + if (cppBase->internalName() == u"QQmlAbstractDelegateComponent") + return true; + return false; +} + +/*! + \internal + Returns true if the scope is the outermost element of a separate Component + Either because it has been implicitly wrapped, e.g. due to an assignment to + a Component property, or because it is the first (and only) child of a + Component. + */ +bool QQmlJSScope::isComponentRootElement() const { + if (m_flags.testFlag(WrappedInImplicitComponent)) + return true; + auto base = nonCompositeBaseType(parentScope()); // handles null parentScope() + if (!base) + return false; + return base->internalName() == u"QQmlComponent"; +} + bool QQmlJSScope::isIdInCurrentQmlScopes(const QString &id) const { if (m_scopeType == QQmlJSScope::QMLScope) @@ -602,13 +641,7 @@ bool QQmlJSScope::canAssign(const QQmlJSScope::ConstPtr &derived) const if (!derived) return false; - bool isBaseComponent = false; - for (auto scope = this; scope; scope = scope->baseType().get()) { - if (internalName() == u"QQmlComponent"_qs) { - isBaseComponent = true; - break; - } - } + bool isBaseComponent = causesImplicitComponentWrapping(); for (auto scope = derived; !scope.isNull(); scope = scope->baseType()) { if (isSameType(scope)) diff --git a/src/qmlcompiler/qqmljsscope_p.h b/src/qmlcompiler/qqmljsscope_p.h index addf036df8..acc837d2a2 100644 --- a/src/qmlcompiler/qqmljsscope_p.h +++ b/src/qmlcompiler/qqmljsscope_p.h @@ -115,7 +115,8 @@ public: Script = 0x8, CustomParser = 0x10, Array = 0x20, - InlineComponent = 0x40 + InlineComponent = 0x40, + WrappedInImplicitComponent = 0x80 }; Q_DECLARE_FLAGS(Flags, Flag) Q_FLAGS(Flags); @@ -201,6 +202,9 @@ public: QString internalName() const { return m_internalName; } void setInternalName(const QString &internalName) { m_internalName = internalName; } + bool causesImplicitComponentWrapping() const; + bool isComponentRootElement() const; + void addExport(const QString &name, const QString &package, const QTypeRevision &version); QList exports() const { return m_exports; } @@ -295,6 +299,7 @@ public: } void setIsArrayScope(bool v) { m_flags.setFlag(Array, v); } void setIsInlineComponent(bool v) { m_flags.setFlag(InlineComponent, v); } + void setIsWrappedInImplicitComponent(bool v) { m_flags.setFlag(WrappedInImplicitComponent, v); } void setAccessSemantics(AccessSemantics semantics) { m_semantics = semantics; } AccessSemantics accessSemantics() const { return m_semantics; } diff --git a/tests/auto/qml/qmllint/data/requiredPropertyInComponent.qml b/tests/auto/qml/qmllint/data/requiredPropertyInComponent.qml new file mode 100644 index 0000000000..24ecf9d706 --- /dev/null +++ b/tests/auto/qml/qmllint/data/requiredPropertyInComponent.qml @@ -0,0 +1,25 @@ +import QtQuick 2.0 + +Row { + // explicit component + Component { + id: foo + Item { + required property int i + } + } + + // implicit component, plain property + property Component com: Item {} + + Repeater { + model: 3 + // implicit component, default property + Text { + required property int index + height: 40 + color: "black" + text: "I'm item " + index + } + } +} diff --git a/tests/auto/qml/qmllint/tst_qmllint.cpp b/tests/auto/qml/qmllint/tst_qmllint.cpp index f6fc89e571..1a317e6244 100644 --- a/tests/auto/qml/qmllint/tst_qmllint.cpp +++ b/tests/auto/qml/qmllint/tst_qmllint.cpp @@ -820,6 +820,7 @@ void TestQmllint::cleanQmlCode_data() QTest::newRow("QQmlEasingEnums::Type") << QStringLiteral("animationEasing.qml"); QTest::newRow("ValidLiterals") << QStringLiteral("validLiterals.qml"); QTest::newRow("GoodModulePrefix") << QStringLiteral("goodModulePrefix.qml"); + QTest::newRow("required property in Component") << QStringLiteral("requiredPropertyInComponent.qml"); } void TestQmllint::cleanQmlCode()