diff options
39 files changed, 613 insertions, 45 deletions
diff --git a/src/qml/common/qv4compileddata_p.h b/src/qml/common/qv4compileddata_p.h index c3ddce5884..11de506a53 100644 --- a/src/qml/common/qv4compileddata_p.h +++ b/src/qml/common/qv4compileddata_p.h @@ -75,7 +75,7 @@ QT_BEGIN_NAMESPACE // Also change the comment behind the number to describe the latest change. This has the added // benefit that if another patch changes the version too, it will result in a merge conflict, and // not get removed silently. -#define QV4_DATA_STRUCTURE_VERSION 0x24 // Collect function parameter types +#define QV4_DATA_STRUCTURE_VERSION 0x26// support required properties class QIODevice; class QQmlTypeNameCache; @@ -656,7 +656,8 @@ struct Property { quint32_le nameIndex; union { - quint32_le_bitfield<0, 29> builtinTypeOrTypeNameIndex; + quint32_le_bitfield<0, 28> builtinTypeOrTypeNameIndex; + quint32_le_bitfield<28, 1> isRequired; quint32_le_bitfield<29, 1> isBuiltinType; quint32_le_bitfield<30, 1> isList; quint32_le_bitfield<31, 1> isReadOnly; diff --git a/src/qml/compiler/qqmlirbuilder.cpp b/src/qml/compiler/qqmlirbuilder.cpp index afe68b72cb..fbb6a07b1b 100644 --- a/src/qml/compiler/qqmlirbuilder.cpp +++ b/src/qml/compiler/qqmlirbuilder.cpp @@ -847,6 +847,7 @@ bool IRBuilder::visit(QQmlJS::AST::UiPublicMember *node) Property *property = New<Property>(); property->isReadOnly = node->isReadonlyMember; + property->isRequired = node->isRequired; QV4::CompiledData::BuiltinType builtinPropertyType = Parameter::stringToBuiltinType(memberType); bool typeFound = builtinPropertyType != QV4::CompiledData::BuiltinType::InvalidBuiltin; diff --git a/src/qml/parser/qqmljs.g b/src/qml/parser/qqmljs.g index b54232442c..cc560c5912 100644 --- a/src/qml/parser/qqmljs.g +++ b/src/qml/parser/qqmljs.g @@ -89,6 +89,7 @@ %token T_STATIC "static" %token T_EXPORT "export" %token T_FROM "from" +%token T_REQUIRED "required" --- template strings %token T_NO_SUBSTITUTION_TEMPLATE"(no subst template)" @@ -121,7 +122,7 @@ %token T_FOR_LOOKAHEAD_OK "(for lookahead ok)" --%left T_PLUS T_MINUS -%nonassoc T_IDENTIFIER T_COLON T_SIGNAL T_PROPERTY T_READONLY T_ON T_SET T_GET T_OF T_STATIC T_FROM T_AS +%nonassoc T_IDENTIFIER T_COLON T_SIGNAL T_PROPERTY T_READONLY T_ON T_SET T_GET T_OF T_STATIC T_FROM T_AS T_REQUIRED %nonassoc REDUCE_HERE %right T_THEN T_ELSE @@ -1216,7 +1217,6 @@ UiObjectMemberPropertyNoInitialiser: T_PROPERTY UiPropertyType QmlIdentifier Sem ./ - UiObjectMember: UiObjectMemberPropertyNoInitialiser; UiObjectMember: T_DEFAULT UiObjectMemberPropertyNoInitialiser; @@ -1245,6 +1245,17 @@ OptionalSemicolon: | Semicolon; and then we would miss a semicolon (see tests/auto/quick/qquickvisualdatamodel/data/objectlist.qml)*/ ./ +UiObjectMember: T_REQUIRED UiObjectMemberPropertyNoInitialiser; +/. + case $rule_number: { + AST::UiPublicMember *node = sym(2).UiPublicMember; + node->requiredToken = loc(1); + node->isRequired = true; + sym(1).Node = node; + } break; +./ + + UiObjectMemberWithScriptStatement: T_PROPERTY UiPropertyType QmlIdentifier T_COLON UiScriptStatement OptionalSemicolon; /. case $rule_number: { @@ -1459,6 +1470,7 @@ QmlIdentifier: T_GET; QmlIdentifier: T_SET; QmlIdentifier: T_FROM; QmlIdentifier: T_OF; +QmlIdentifier: T_REQUIRED; JsIdentifier: T_IDENTIFIER; JsIdentifier: T_PROPERTY; @@ -1471,6 +1483,7 @@ JsIdentifier: T_FROM; JsIdentifier: T_STATIC; JsIdentifier: T_OF; JsIdentifier: T_AS; +JsIdentifier: T_REQUIRED; IdentifierReference: JsIdentifier; BindingIdentifier: IdentifierReference; diff --git a/src/qml/parser/qqmljsast_p.h b/src/qml/parser/qqmljsast_p.h index 6c28903066..4247785905 100644 --- a/src/qml/parser/qqmljsast_p.h +++ b/src/qml/parser/qqmljsast_p.h @@ -3308,6 +3308,8 @@ public: return defaultToken; else if (readonlyToken.isValid()) return readonlyToken; + else if (requiredToken.isValid()) + return requiredToken; return propertyToken; } @@ -3331,10 +3333,13 @@ public: UiObjectMember *binding; // initialized with a QML object or array. bool isDefaultMember; bool isReadonlyMember; + bool isRequired = false; UiParameterList *parameters; + // TODO: merge source locations SourceLocation defaultToken; SourceLocation readonlyToken; SourceLocation propertyToken; + SourceLocation requiredToken; SourceLocation typeModifierToken; SourceLocation typeToken; SourceLocation identifierToken; diff --git a/src/qml/parser/qqmljskeywords_p.h b/src/qml/parser/qqmljskeywords_p.h index 96b3709162..3eb054341f 100644 --- a/src/qml/parser/qqmljskeywords_p.h +++ b/src/qml/parser/qqmljskeywords_p.h @@ -743,6 +743,18 @@ static inline int classify8(const QChar *s, int parseModeFlags) { } } } + } else if (s[2].unicode() == 'q') { + if (s[3].unicode() == 'u') { + if (s[4].unicode() == 'i') { + if (s[5].unicode() == 'r') { + if (s[6].unicode() == 'e') { + if (s[7].unicode() == 'd') { + return Lexer::T_REQUIRED; + } + } + } + } + } } } } diff --git a/src/qml/qml/qqmlcomponent.cpp b/src/qml/qml/qqmlcomponent.cpp index ed8c41a582..b72c745490 100644 --- a/src/qml/qml/qqmlcomponent.cpp +++ b/src/qml/qml/qqmlcomponent.cpp @@ -340,6 +340,11 @@ void QQmlComponentPrivate::fromTypeData(const QQmlRefPointer<QQmlTypeData> &data } } +RequiredProperties &QQmlComponentPrivate::requiredProperties() +{ + return state.creator->requiredProperties(); +} + void QQmlComponentPrivate::clear() { if (typeData) { @@ -364,8 +369,8 @@ QObject *QQmlComponentPrivate::doBeginCreate(QQmlComponent *q, QQmlContext *cont bool QQmlComponentPrivate::setInitialProperty(QObject *component, const QString& name, const QVariant &value) { - QQmlProperty prop(component, name); - auto privProp = QQmlPropertyPrivate::get(prop); + QQmlProperty prop = QQmlComponentPrivate::removePropertyFromRequired(component, name, requiredProperties()); + QQmlPropertyPrivate *privProp = QQmlPropertyPrivate::get(prop); if (!prop.isValid() || !privProp->writeValueProperty(value, nullptr)) { QQmlError error{}; error.setUrl(url); @@ -809,6 +814,10 @@ QObject *QQmlComponent::create(QQmlContext *context) QObject *rv = d->doBeginCreate(this, context); if (rv) completeCreate(); + if (rv && !d->requiredProperties().empty()) { + delete rv; + return nullptr; + } return rv; } @@ -828,6 +837,10 @@ QObject *QQmlComponent::createWithInitialProperties(const QVariantMap& initialPr setInitialProperties(rv, initialProperties); completeCreate(); } + if (!d->requiredProperties().empty()) { + d->requiredProperties().clear(); + return nullptr; + } return rv; } @@ -980,6 +993,57 @@ void QQmlComponentPrivate::complete(QQmlEnginePrivate *enginePriv, ConstructionS } /*! + * \internal + * Finds the matching toplevel property with name \a name of the component \a createdComponent. + * If it was a required property or an alias to a required property contained in \a + * requiredProperties, it is removed from it. + * + * If wasInRequiredProperties is non-null, the referenced boolean is set to true iff the property + * was found in requiredProperties. + * + * Returns the QQmlProperty with name \a name (which might be invalid if there is no such property), + * for further processing (for instance, actually setting the property value). + * + * Note: This method is used in QQmlComponent and QQmlIncubator to manage required properties. Most + * classes which create components should not need it and should only need to call + * setInitialProperties. + */ +QQmlProperty QQmlComponentPrivate::removePropertyFromRequired(QObject *createdComponent, const QString &name, RequiredProperties &requiredProperties, bool* wasInRequiredProperties) +{ + QQmlProperty prop(createdComponent, name); + auto privProp = QQmlPropertyPrivate::get(prop); + if (prop.isValid()) { + // resolve outstanding required properties + auto targetProp = &privProp->core; + if (targetProp->isAlias()) { + auto target = createdComponent; + QQmlPropertyIndex originalIndex(targetProp->coreIndex()); + QQmlPropertyIndex propIndex; + QQmlPropertyPrivate::findAliasTarget(target, originalIndex, &target, &propIndex); + QQmlData *data = QQmlData::get(target); + Q_ASSERT(data && data->propertyCache); + targetProp = data->propertyCache->property(propIndex.coreIndex()); + } else { + // we need to get the pointer from the property cache instead of directly using + // targetProp else the lookup will fail + QQmlData *data = QQmlData::get(createdComponent); + Q_ASSERT(data && data->propertyCache); + targetProp = data->propertyCache->property(targetProp->coreIndex()); + } + auto it = requiredProperties.find(targetProp); + if (it != requiredProperties.end()) { + if (wasInRequiredProperties) + *wasInRequiredProperties = true; + requiredProperties.erase(it); + } else { + if (wasInRequiredProperties) + *wasInRequiredProperties = false; + } + } + return prop; +} + +/*! This method provides advanced control over component instance creation. In general, programmers should use QQmlComponent::create() to create a component. @@ -998,6 +1062,11 @@ void QQmlComponent::completeCreate() void QQmlComponentPrivate::completeCreate() { + const RequiredProperties& unsetRequiredProperties = requiredProperties(); + for (const auto& unsetRequiredProperty: unsetRequiredProperties) { + QQmlError error = unsetRequiredPropertyToQQmlError(unsetRequiredProperty); + state.errors.push_back(error); + } if (state.completePending) { ++creationDepth.localData(); QQmlEnginePrivate *ep = QQmlEnginePrivate::get(engine); @@ -1185,7 +1254,7 @@ struct QmlIncubatorObject : public QV4::Object static ReturnedValue method_forceCompletion(const FunctionObject *, const Value *thisObject, const Value *argv, int argc); void statusChanged(QQmlIncubator::Status); - void setInitialState(QObject *); + void setInitialState(QObject *, RequiredProperties &requiredProperties); }; } @@ -1210,7 +1279,8 @@ public: void setInitialState(QObject *o) override { QV4::Scope scope(incubatorObject.engine()); QV4::Scoped<QV4::QmlIncubatorObject> i(scope, incubatorObject.as<QV4::QmlIncubatorObject>()); - i->setInitialState(o); + auto d = QQmlIncubatorPrivate::get(this); + i->setInitialState(o, d->requiredProperties()); } QV4::PersistentValue incubatorObject; // keep a strong internal reference while incubating @@ -1282,7 +1352,7 @@ static void QQmlComponent_setQmlParent(QObject *me, QObject *parent) */ -void QQmlComponentPrivate::setInitialProperties(QV4::ExecutionEngine *engine, QV4::QmlContext *qmlContext, const QV4::Value &o, const QV4::Value &v) +void QQmlComponentPrivate::setInitialProperties(QV4::ExecutionEngine *engine, QV4::QmlContext *qmlContext, const QV4::Value &o, const QV4::Value &v, RequiredProperties &requiredProperties, QObject *createdComponent) { QV4::Scope scope(engine); QV4::ScopedObject object(scope); @@ -1301,6 +1371,7 @@ void QQmlComponentPrivate::setInitialProperties(QV4::ExecutionEngine *engine, QV break; object = o; const QStringList properties = name->toQString().split(QLatin1Char('.')); + bool isTopLevelProperty = properties.size() == 1; for (int i = 0; i < properties.length() - 1; ++i) { name = engine->newString(properties.at(i)); object = object->get(name); @@ -1317,12 +1388,40 @@ void QQmlComponentPrivate::setInitialProperties(QV4::ExecutionEngine *engine, QV if (engine->hasException) { engine->hasException = false; continue; + } else if (isTopLevelProperty) { + auto prop = removePropertyFromRequired(createdComponent, name->toQString(), requiredProperties); } } engine->hasException = false; } +QQmlError QQmlComponentPrivate::unsetRequiredPropertyToQQmlError(const RequiredPropertyInfo &unsetRequiredProperty) +{ + QQmlError error; + QString description = QLatin1String("Required property %1 was not initialized").arg(unsetRequiredProperty.propertyName); + switch (unsetRequiredProperty.aliasesToRequired.size()) { + case 0: + break; + case 1: { + const auto info = unsetRequiredProperty.aliasesToRequired.first(); + description += QLatin1String("\nIt can be set via the alias property %1 from %2\n").arg(info.propertyName, info.fileUrl.toString()); + break; + } + default: + description += QLatin1String("\nIt can be set via one of the following alias properties:"); + for (auto aliasInfo: unsetRequiredProperty.aliasesToRequired) { + description += QLatin1String("\n- %1 (%2)").arg(aliasInfo.propertyName, aliasInfo.fileUrl.toString()); + } + description += QLatin1Char('\n'); + } + error.setDescription(description); + error.setUrl(unsetRequiredProperty.fileUrl); + error.setLine(unsetRequiredProperty.location.line); + error.setColumn(unsetRequiredProperty.location.column); + return error; +} + /*! \internal */ @@ -1370,7 +1469,17 @@ void QQmlComponent::createObject(QQmlV4Function *args) if (!valuemap->isUndefined()) { QV4::Scoped<QV4::QmlContext> qmlContext(scope, v4->qmlContext()); - QQmlComponentPrivate::setInitialProperties(v4, qmlContext, object, valuemap); + QQmlComponentPrivate::setInitialProperties(v4, qmlContext, object, valuemap, d->requiredProperties(), rv); + } + if (!d->requiredProperties().empty()) { + QList<QQmlError> errors; + for (const auto &requiredProperty: d->requiredProperties()) { + errors.push_back(QQmlComponentPrivate::unsetRequiredPropertyToQQmlError(requiredProperty)); + } + qmlWarning(rv, errors); + args->setReturnValue(QV4::Encode::null()); + delete rv; + return; } d->completeCreate(); @@ -1502,7 +1611,7 @@ void QQmlComponent::incubateObject(QQmlV4Function *args) } // XXX used by QSGLoader -void QQmlComponentPrivate::initializeObjectWithInitialProperties(QV4::QmlContext *qmlContext, const QV4::Value &valuemap, QObject *toCreate) +void QQmlComponentPrivate::initializeObjectWithInitialProperties(QV4::QmlContext *qmlContext, const QV4::Value &valuemap, QObject *toCreate, RequiredProperties &requiredProperties) { QV4::ExecutionEngine *v4engine = engine->handle(); QV4::Scope scope(v4engine); @@ -1511,7 +1620,7 @@ void QQmlComponentPrivate::initializeObjectWithInitialProperties(QV4::QmlContext Q_ASSERT(object->as<QV4::Object>()); if (!valuemap.isUndefined()) - setInitialProperties(v4engine, qmlContext, object, valuemap); + setInitialProperties(v4engine, qmlContext, object, valuemap, requiredProperties, toCreate); } QQmlComponentExtension::QQmlComponentExtension(QV4::ExecutionEngine *v4) @@ -1601,7 +1710,7 @@ void QV4::Heap::QmlIncubatorObject::destroy() { Object::destroy(); } -void QV4::QmlIncubatorObject::setInitialState(QObject *o) +void QV4::QmlIncubatorObject::setInitialState(QObject *o, RequiredProperties &requiredProperties) { QQmlComponent_setQmlParent(o, d()->parent); @@ -1610,7 +1719,7 @@ void QV4::QmlIncubatorObject::setInitialState(QObject *o) QV4::Scope scope(v4); QV4::ScopedObject obj(scope, QV4::QObjectWrapper::wrap(v4, o)); QV4::Scoped<QV4::QmlContext> qmlCtxt(scope, d()->qmlContext); - QQmlComponentPrivate::setInitialProperties(v4, qmlCtxt, obj, d()->valuemap); + QQmlComponentPrivate::setInitialProperties(v4, qmlCtxt, obj, d()->valuemap, requiredProperties, o); } } diff --git a/src/qml/qml/qqmlcomponent_p.h b/src/qml/qml/qqmlcomponent_p.h index 2170646b89..8ae7672c19 100644 --- a/src/qml/qml/qqmlcomponent_p.h +++ b/src/qml/qml/qqmlcomponent_p.h @@ -86,8 +86,9 @@ public: QObject *beginCreate(QQmlContextData *); void completeCreate(); - void initializeObjectWithInitialProperties(QV4::QmlContext *qmlContext, const QV4::Value &valuemap, QObject *toCreate); - static void setInitialProperties(QV4::ExecutionEngine *engine, QV4::QmlContext *qmlContext, const QV4::Value &o, const QV4::Value &v); + void initializeObjectWithInitialProperties(QV4::QmlContext *qmlContext, const QV4::Value &valuemap, QObject *toCreate, RequiredProperties &requiredProperties); + static void setInitialProperties(QV4::ExecutionEngine *engine, QV4::QmlContext *qmlContext, const QV4::Value &o, const QV4::Value &v, RequiredProperties &requiredProperties, QObject *createdComponent); + static QQmlError unsetRequiredPropertyToQQmlError(const RequiredPropertyInfo &unsetRequiredProperty); virtual void incubateObject( QQmlIncubator *incubationTask, @@ -106,6 +107,7 @@ public: qreal progress; int start; + RequiredProperties& requiredProperties(); QQmlRefPointer<QV4::ExecutableCompilationUnit> compilationUnit; struct ConstructionState { @@ -134,6 +136,7 @@ public: static void completeDeferred(QQmlEnginePrivate *enginePriv, DeferredState *deferredState); static void complete(QQmlEnginePrivate *enginePriv, ConstructionState *state); + static QQmlProperty removePropertyFromRequired(QObject *createdComponent, const QString &name, RequiredProperties& requiredProperties, bool *wasInRequiredProperties = nullptr); QQmlEngine *engine; QQmlGuardedContextData creationContext; diff --git a/src/qml/qml/qqmlincubator.cpp b/src/qml/qml/qqmlincubator.cpp index bc06226cbf..7b3ae31c08 100644 --- a/src/qml/qml/qqmlincubator.cpp +++ b/src/qml/qml/qqmlincubator.cpp @@ -43,6 +43,7 @@ #include "qqmlexpression_p.h" #include "qqmlobjectcreator_p.h" +#include <private/qqmlcomponent_p.h> void QQmlEnginePrivate::incubate(QQmlIncubator &i, QQmlContextData *forContext) { @@ -296,6 +297,20 @@ void QQmlIncubatorPrivate::incubate(QQmlInstantiationInterrupt &i) tresult = creator->create(subComponentToCreate, /*parent*/nullptr, &i); if (!tresult) errors = creator->errors; + else { + RequiredProperties& requiredProperties = creator->requiredProperties(); + for (auto it = initialProperties.cbegin(); it != initialProperties.cend(); ++it) { + auto component = tresult; + auto name = it.key(); + QQmlProperty prop = QQmlComponentPrivate::removePropertyFromRequired(component, name, requiredProperties); + if (!prop.isValid() || !prop.write(it.value())) { + QQmlError error{}; + error.setUrl(compilationUnit->url()); + error.setDescription(QLatin1String("Could not set property %1").arg(name)); + errors.push_back(error); + } + } + } enginePriv->dereferenceScarceResources(); if (watcher.hasRecursed()) @@ -312,8 +327,14 @@ void QQmlIncubatorPrivate::incubate(QQmlInstantiationInterrupt &i) ddata->indestructible = true; ddata->explicitIndestructibleSet = true; ddata->rootObjectInCreation = false; - if (q) + if (q) { q->setInitialState(result); + if (!creator->requiredProperties().empty()) { + const auto& unsetRequiredProperties = creator->requiredProperties(); + for (const auto& unsetRequiredProperty: unsetRequiredProperties) + errors << QQmlComponentPrivate::unsetRequiredPropertyToQQmlError(unsetRequiredProperty); + } + } } if (watcher.hasRecursed()) @@ -657,6 +678,31 @@ QObject *QQmlIncubator::object() const } /*! +Return a list of properties which are required but haven't been set yet. +This list can be modified, so that subclasses which implement special logic +setInitialProperties can mark properties set there as no longer required. + +\sa QQmlIncubator::setInitialProperties +\since 5.15 +*/ +RequiredProperties &QQmlIncubatorPrivate::requiredProperties() +{ + return creator->requiredProperties(); +} + +/*! +Stores a mapping from property names to initial values with which the incubated +component will be initialized + +\sa QQmlComponent::setInitialProperties +\since 5.15 +*/ +void QQmlIncubator::setInitialProperties(const QVariantMap &initialProperties) +{ + d->initialProperties = initialProperties; +} + +/*! Called when the status of the incubator changes. \a status is the new status. The default implementation does nothing. diff --git a/src/qml/qml/qqmlincubator.h b/src/qml/qml/qqmlincubator.h index e68f6e3c45..f075407e73 100644 --- a/src/qml/qml/qqmlincubator.h +++ b/src/qml/qml/qqmlincubator.h @@ -47,6 +47,9 @@ QT_BEGIN_NAMESPACE class QQmlEngine; +class QQmlPropertyData; +class QVariant; +using QVariantMap = QMap<QString, QVariant>; class QQmlIncubatorPrivate; class Q_QML_EXPORT QQmlIncubator @@ -84,6 +87,8 @@ public: QObject *object() const; + void setInitialProperties(const QVariantMap &initialProperties); + protected: virtual void statusChanged(Status); virtual void setInitialState(QObject *); diff --git a/src/qml/qml/qqmlincubator_p.h b/src/qml/qml/qqmlincubator_p.h index 57ec8249cb..731db7aad3 100644 --- a/src/qml/qml/qqmlincubator_p.h +++ b/src/qml/qml/qqmlincubator_p.h @@ -61,8 +61,12 @@ QT_BEGIN_NAMESPACE +class QQmlPropertyData; +struct RequiredPropertyInfo; +using RequiredProperties = QHash<QQmlPropertyData*, RequiredPropertyInfo>; + class QQmlIncubator; -class QQmlIncubatorPrivate : public QQmlEnginePrivate::Incubator +class Q_QML_PRIVATE_EXPORT QQmlIncubatorPrivate : public QQmlEnginePrivate::Incubator { public: QQmlIncubatorPrivate(QQmlIncubator *q, QQmlIncubator::IncubationMode m); @@ -97,11 +101,13 @@ public: QIntrusiveList<QIPBase, &QIPBase::nextWaitingFor> waitingFor; QRecursionNode recursion; + QVariantMap initialProperties; void clear(); void forceCompletion(QQmlInstantiationInterrupt &i); void incubate(QQmlInstantiationInterrupt &i); + RequiredProperties &requiredProperties(); }; QT_END_NAMESPACE diff --git a/src/qml/qml/qqmlobjectcreator.cpp b/src/qml/qml/qqmlobjectcreator.cpp index f89608cd5d..b723ddb381 100644 --- a/src/qml/qml/qqmlobjectcreator.cpp +++ b/src/qml/qml/qqmlobjectcreator.cpp @@ -783,6 +783,23 @@ void QQmlObjectCreator::setupBindings(bool applyDeferredBindings) const QV4::CompiledData::Binding *binding = _compiledObject->bindingTable(); for (quint32 i = 0; i < _compiledObject->nBindings; ++i, ++binding) { + QQmlPropertyData *const property = propertyData.at(i); + if (property) { + QQmlPropertyData* targetProperty = property; + if (targetProperty->isAlias()) { + // follow alias + auto target = _bindingTarget; + QQmlPropertyIndex originalIndex(targetProperty->coreIndex(), _valueTypeProperty ? _valueTypeProperty->coreIndex() : -1); + QQmlPropertyIndex propIndex; + QQmlPropertyPrivate::findAliasTarget(target, originalIndex, &target, &propIndex); + QQmlData *data = QQmlData::get(target); + Q_ASSERT(data && data->propertyCache); + targetProperty = data->propertyCache->property(propIndex.coreIndex()); + } + sharedState->requiredProperties.remove(targetProperty); + } + + if (binding->flags & QV4::CompiledData::Binding::IsCustomParserBinding) continue; @@ -794,8 +811,6 @@ void QQmlObjectCreator::setupBindings(bool applyDeferredBindings) continue; } - const QQmlPropertyData *property = propertyData.at(i); - if (property && property->isQList()) { if (property->coreIndex() != currentListPropertyIndex) { void *argv[1] = { (void*)&_currentList }; @@ -1504,10 +1519,42 @@ bool QQmlObjectCreator::populateInstance(int index, QObject *instance, QObject * if (_compiledObject->flags & QV4::CompiledData::Object::HasDeferredBindings) _ddata->deferData(_compiledObjectIndex, compilationUnit, context); + for (int propertyIndex = 0; propertyIndex != _compiledObject->propertyCount(); ++propertyIndex) { + const QV4::CompiledData::Property* property = _compiledObject->propertiesBegin() + propertyIndex; + QQmlPropertyData *propertyData = _propertyCache->property(_propertyCache->propertyOffset() + propertyIndex); + if (property->isRequired) { + sharedState->requiredProperties.insert(propertyData, + RequiredPropertyInfo {compilationUnit->stringAt(property->nameIndex), compilationUnit->finalUrl(), property->location, {}}); + } + } + if (_compiledObject->nFunctions > 0) setupFunctions(); setupBindings(); + for (int aliasIndex = 0; aliasIndex != _compiledObject->aliasCount(); ++aliasIndex) { + const QV4::CompiledData::Alias* alias = _compiledObject->aliasesBegin() + aliasIndex; + const auto originalAlias = alias; + while (alias->aliasToLocalAlias) + alias = _compiledObject->aliasesBegin() + alias->localAliasIndex; + Q_ASSERT(alias->flags & QV4::CompiledData::Alias::Resolved); + if (!context->idValues->wasSet()) + continue; + QObject *target = context->idValues[alias->targetObjectId].data(); + if (!target) + continue; + QQmlData *targetDData = QQmlData::get(target, /*create*/false); + if (!targetDData) + continue; + int coreIndex = QQmlPropertyIndex::fromEncoded(alias->encodedMetaPropertyIndex).coreIndex(); + QQmlPropertyData *const targetProperty = targetDData->propertyCache->property(coreIndex); + if (!targetProperty) + continue; + auto it = sharedState->requiredProperties.find(targetProperty); + if (it != sharedState->requiredProperties.end()) + it->aliasesToRequired.push_back(AliasToRequiredInfo {compilationUnit->stringAt(originalAlias->nameIndex), compilationUnit->finalUrl()}); + } + qSwap(_vmeMetaObject, vmeMetaObject); qSwap(_bindingTarget, bindingTarget); qSwap(_ddata, declarativeData); diff --git a/src/qml/qml/qqmlobjectcreator_p.h b/src/qml/qml/qqmlobjectcreator_p.h index ecdbcc56dd..ee1d82d4e3 100644 --- a/src/qml/qml/qqmlobjectcreator_p.h +++ b/src/qml/qml/qqmlobjectcreator_p.h @@ -66,6 +66,28 @@ class QQmlAbstractBinding; class QQmlInstantiationInterrupt; class QQmlIncubatorPrivate; +struct AliasToRequiredInfo { + QString propertyName; + QUrl fileUrl; +}; + +/*! +\internal +This struct contains information solely used for displaying error messages +\variable aliasesToRequired allows us to give the user a way to know which (aliasing) properties +can be set to set the required property +\sa QQmlComponentPrivate::unsetRequiredPropertyToQQmlError +*/ +struct RequiredPropertyInfo +{ + QString propertyName; + QUrl fileUrl; + QV4::CompiledData::Location location; + QVector<AliasToRequiredInfo> aliasesToRequired; +}; + +using RequiredProperties = QHash<QQmlPropertyData*, RequiredPropertyInfo>; + struct QQmlObjectCreatorSharedState : public QSharedData { QQmlContextData *rootContext; @@ -78,6 +100,7 @@ struct QQmlObjectCreatorSharedState : public QSharedData QList<QQmlEnginePrivate::FinalizeCallback> finalizeCallbacks; QQmlVmeProfiler profiler; QRecursionNode recursionNode; + RequiredProperties requiredProperties; }; class Q_QML_PRIVATE_EXPORT QQmlObjectCreator @@ -102,6 +125,8 @@ public: QQmlContextData *parentContextData() const { return parentContext.contextData(); } QFiniteStack<QPointer<QObject> > &allCreatedObjects() { return sharedState->allCreatedObjects; } + RequiredProperties &requiredProperties() {return sharedState->requiredProperties;} + private: QQmlObjectCreator(QQmlContextData *contextData, const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit, QQmlObjectCreatorSharedState *inheritedSharedState); diff --git a/src/quick/items/qquickloader.cpp b/src/quick/items/qquickloader.cpp index d0e29c204e..819a3a73e3 100644 --- a/src/quick/items/qquickloader.cpp +++ b/src/quick/items/qquickloader.cpp @@ -45,6 +45,7 @@ #include <private/qqmlglobal_p.h> #include <private/qqmlcomponent_p.h> +#include <private/qqmlincubator_p.h> QT_BEGIN_NAMESPACE @@ -664,7 +665,8 @@ void QQuickLoaderPrivate::setInitialState(QObject *obj) QV4::Scope scope(v4); QV4::ScopedValue ipv(scope, initialPropertyValues.value()); QV4::Scoped<QV4::QmlContext> qmlContext(scope, qmlCallingContext.value()); - d->initializeObjectWithInitialProperties(qmlContext, ipv, obj); + auto incubatorPriv = QQmlIncubatorPrivate::get(incubator); + d->initializeObjectWithInitialProperties(qmlContext, ipv, obj, incubatorPriv->requiredProperties()); } void QQuickLoaderIncubator::statusChanged(Status status) diff --git a/tests/auto/qml/qmlmin/tst_qmlmin.cpp b/tests/auto/qml/qmlmin/tst_qmlmin.cpp index cae833cd60..79a73299a4 100644 --- a/tests/auto/qml/qmlmin/tst_qmlmin.cpp +++ b/tests/auto/qml/qmlmin/tst_qmlmin.cpp @@ -130,6 +130,8 @@ void tst_qmlmin::initTestCase() invalidFiles << "tests/auto/qml/qjsengine/script/com/trolltech/syntaxerror/__init__.js"; invalidFiles << "tests/auto/qml/debugger/qqmlpreview/data/broken.qml"; invalidFiles << "tests/auto/qml/qqmllanguage/data/fuzzed.2.qml"; + invalidFiles << "tests/auto/qml/qqmllanguage/data/requiredProperties.2.qml"; + invalidFiles << "tests/auto/qml/qqmllanguage/data/requiredProperties.3.qml"; // generatorFunction.qml is not invalid per se, but the minifier cannot handle yield statements invalidFiles << "tests/auto/qml/qqmlecmascript/data/generatorFunction.qml"; #endif diff --git a/tests/auto/qml/qqmlcomponent/data/AliasToSubcomponentRequiredBase.qml b/tests/auto/qml/qqmlcomponent/data/AliasToSubcomponentRequiredBase.qml new file mode 100644 index 0000000000..f693cb4c2e --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/AliasToSubcomponentRequiredBase.qml @@ -0,0 +1,10 @@ +import QtQuick 2.13 + +Item { + property alias i_alias: sub.i + + Item { + id: sub + required property int i + } +} diff --git a/tests/auto/qml/qqmlcomponent/data/BaseWithRequired.qml b/tests/auto/qml/qqmlcomponent/data/BaseWithRequired.qml new file mode 100644 index 0000000000..0ce61c8d9d --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/BaseWithRequired.qml @@ -0,0 +1,6 @@ +import QtQuick 2.14 + +Item { + id: base + required property int i +} diff --git a/tests/auto/qml/qqmlcomponent/data/aliasToSubcomponentNotSet.qml b/tests/auto/qml/qqmlcomponent/data/aliasToSubcomponentNotSet.qml new file mode 100644 index 0000000000..527eb417e5 --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/aliasToSubcomponentNotSet.qml @@ -0,0 +1,4 @@ +import QtQuick 2.13 + +AliasToSubcomponentRequiredBase { +} diff --git a/tests/auto/qml/qqmlcomponent/data/createdFromQml.qml b/tests/auto/qml/qqmlcomponent/data/createdFromQml.qml new file mode 100644 index 0000000000..60a2077606 --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/createdFromQml.qml @@ -0,0 +1,11 @@ +import QtQuick 2.14 + +Item { + id: root + property Item it + Component.onCompleted: function() { + let component = Qt.createComponent("requiredNotSet.qml", Component.PreferSynchronous, root) + console.assert(component.status == Component.Ready) + it = component.createObject(component, {i: 42}) + } +} diff --git a/tests/auto/qml/qqmlcomponent/data/createdFromQmlFail.qml b/tests/auto/qml/qqmlcomponent/data/createdFromQmlFail.qml new file mode 100644 index 0000000000..e09ddcccc1 --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/createdFromQmlFail.qml @@ -0,0 +1,11 @@ +import QtQuick 2.14 + +Item { + id: root + property Item it + Component.onCompleted: function() { + let component = Qt.createComponent("requiredNotSet.qml", Component.PreferSynchronous, root) + console.assert(component.status == Component.Ready) + root.it = component.createObject(component) + } +} diff --git a/tests/auto/qml/qqmlcomponent/data/requiredNotSet.qml b/tests/auto/qml/qqmlcomponent/data/requiredNotSet.qml new file mode 100644 index 0000000000..c0b5d695f1 --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/requiredNotSet.qml @@ -0,0 +1,5 @@ +import QtQuick 2.14 + +Item { + required property int i +} diff --git a/tests/auto/qml/qqmlcomponent/data/requiredSetInSameFile.qml b/tests/auto/qml/qqmlcomponent/data/requiredSetInSameFile.qml new file mode 100644 index 0000000000..76dfcd87e5 --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/requiredSetInSameFile.qml @@ -0,0 +1,6 @@ +import QtQuick 2.14 + +Item { + required property int i + i: 42 +} diff --git a/tests/auto/qml/qqmlcomponent/data/requiredSetLater.qml b/tests/auto/qml/qqmlcomponent/data/requiredSetLater.qml new file mode 100644 index 0000000000..3b7811e453 --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/requiredSetLater.qml @@ -0,0 +1,5 @@ +import QtQuick 2.14 + +BaseWithRequired { + i: 13 +} diff --git a/tests/auto/qml/qqmlcomponent/data/requiredSetViaAliasAfterSameFile.qml b/tests/auto/qml/qqmlcomponent/data/requiredSetViaAliasAfterSameFile.qml new file mode 100644 index 0000000000..163616bc8a --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/requiredSetViaAliasAfterSameFile.qml @@ -0,0 +1,8 @@ +import QtQuick 2.14 + +Item { + id: withAlias + j: 42 + required property int i + property alias j: withAlias.i +} diff --git a/tests/auto/qml/qqmlcomponent/data/requiredSetViaAliasBeforeSameFile.qml b/tests/auto/qml/qqmlcomponent/data/requiredSetViaAliasBeforeSameFile.qml new file mode 100644 index 0000000000..e59bccf379 --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/requiredSetViaAliasBeforeSameFile.qml @@ -0,0 +1,8 @@ +import QtQuick 2.14 + +Item { + id: withAlias + required property int i + j: 42 + property alias j: withAlias.i +} diff --git a/tests/auto/qml/qqmlcomponent/data/requiredSetViaAliasParentFile.qml b/tests/auto/qml/qqmlcomponent/data/requiredSetViaAliasParentFile.qml new file mode 100644 index 0000000000..0bc2f8a7df --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/requiredSetViaAliasParentFile.qml @@ -0,0 +1,7 @@ +import QtQuick 2.14 + +BaseWithRequired { + id: withAlias + property alias j: withAlias.i + j: 42 +} diff --git a/tests/auto/qml/qqmlcomponent/data/requiredSetViaChainedAlias.qml b/tests/auto/qml/qqmlcomponent/data/requiredSetViaChainedAlias.qml new file mode 100644 index 0000000000..45cf02d3d8 --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/requiredSetViaChainedAlias.qml @@ -0,0 +1,9 @@ +import QtQml 2.12 + +QtObject { + id: stuff + required property int x; + property alias y: stuff.x + property alias z: stuff.y + z: 5 +} diff --git a/tests/auto/qml/qqmlcomponent/data/setViaAliasToSubcomponent.qml b/tests/auto/qml/qqmlcomponent/data/setViaAliasToSubcomponent.qml new file mode 100644 index 0000000000..2dc182ea82 --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/setViaAliasToSubcomponent.qml @@ -0,0 +1,5 @@ +import QtQuick 2.13 + +AliasToSubcomponentRequiredBase { + i_alias: 42 +} diff --git a/tests/auto/qml/qqmlcomponent/data/shadowing.qml b/tests/auto/qml/qqmlcomponent/data/shadowing.qml new file mode 100644 index 0000000000..4d119d8884 --- /dev/null +++ b/tests/auto/qml/qqmlcomponent/data/shadowing.qml @@ -0,0 +1,5 @@ +import QtQuick 2.14 + +BaseWithRequired { + property int i: 13 +} diff --git a/tests/auto/qml/qqmlcomponent/tst_qqmlcomponent.cpp b/tests/auto/qml/qqmlcomponent/tst_qqmlcomponent.cpp index 79ec507388..b6cc7fd866 100644 --- a/tests/auto/qml/qqmlcomponent/tst_qqmlcomponent.cpp +++ b/tests/auto/qml/qqmlcomponent/tst_qqmlcomponent.cpp @@ -121,6 +121,9 @@ private slots: void relativeUrl_data(); void relativeUrl(); void setDataNoEngineNoSegfault(); + void testRequiredProperties_data(); + void testRequiredProperties(); + void testRequiredPropertiesFromQml(); void testSetInitialProperties(); private: @@ -668,35 +671,80 @@ void tst_qqmlcomponent::setDataNoEngineNoSegfault() QVERIFY(!c); } -void tst_qqmlcomponent::testSetInitialProperties() +void tst_qqmlcomponent::testRequiredProperties_data() +{ + QTest::addColumn<QUrl>("testFile"); + QTest::addColumn<bool>("shouldSucceed"); + QTest::addColumn<QString>("errorMsg"); + + QTest::addRow("requiredSetViaChainedAlias") << testFileUrl("requiredSetViaChainedAlias.qml") << true << ""; + QTest::addRow("requiredNotSet") << testFileUrl("requiredNotSet.qml") << false << "Required property i was not initialized"; + QTest::addRow("requiredSetInSameFile") << testFileUrl("requiredSetInSameFile.qml") << true << ""; + QTest::addRow("requiredSetViaAlias1") << testFileUrl("requiredSetViaAliasBeforeSameFile.qml") << true << ""; + QTest::addRow("requiredSetViaAlias2") << testFileUrl("requiredSetViaAliasAfterSameFile.qml") << true << ""; + QTest::addRow("requiredSetViaAlias3") << testFileUrl("requiredSetViaAliasParentFile.qml") << true << ""; + QTest::addRow("shadowing") << testFileUrl("shadowing.qml") << false << "Required property i was not initialized"; + QTest::addRow("setLater") << testFileUrl("requiredSetLater.qml") << true << ""; + QTest::addRow("setViaAliasToSubcomponent") << testFileUrl("setViaAliasToSubcomponent.qml") << true << ""; + QTest::addRow("aliasToSubcomponentNotSet") << testFileUrl("aliasToSubcomponentNotSet.qml") << false << "It can be set via the alias property i_alias"; +} + + +void tst_qqmlcomponent::testRequiredProperties() +{ + QQmlEngine eng; + using QScopedObjPointer = QScopedPointer<QObject>; + QFETCH(QUrl, testFile); + QFETCH(bool, shouldSucceed); + QQmlComponent comp(&eng); + comp.loadUrl(testFile); + QScopedObjPointer obj {comp.create()}; + if (shouldSucceed) + QVERIFY(obj); + else { + QVERIFY(!obj); + QFETCH(QString, errorMsg); + QVERIFY(comp.errorString().contains(errorMsg)); + } +} + +void tst_qqmlcomponent::testRequiredPropertiesFromQml() { QQmlEngine eng; { - // JSON based initialization QQmlComponent comp(&eng); - comp.loadUrl(testFileUrl("allJSONTypes.qml")); - QScopedPointer<QObject> obj { comp.beginCreate(eng.rootContext()) }; + comp.loadUrl(testFileUrl("createdFromQml.qml")); + QScopedPointer<QObject> obj { comp.create() }; QVERIFY(obj); - comp.setInitialProperties(obj.get(), QVariantMap { - {QLatin1String("i"), 42}, - {QLatin1String("b"), true}, - {QLatin1String("d"), 3.1416}, - {QLatin1String("s"), QLatin1String("hello world")}, - {QLatin1String("nothing"), QVariant::fromValue(nullptr)} - }); - comp.completeCreate(); - if (!comp.errors().empty()) - qDebug() << comp.errorString() << comp.errors(); - QVERIFY(comp.errors().empty()); - QCOMPARE(obj->property("i"), 42); - QCOMPARE(obj->property("b"), true); - QCOMPARE(obj->property("d"), 3.1416); - QCOMPARE(obj->property("s"), QLatin1String("hello world")); - QCOMPARE(obj->property("nothing"), QVariant::fromValue(nullptr)); + auto root = qvariant_cast<QQuickItem*>(obj->property("it")); + QVERIFY(root); + QCOMPARE(root->property("i").toInt(), 42); } { - // QVariant + QTest::ignoreMessage(QtMsgType::QtWarningMsg, QRegularExpression(".*requiredNotSet.qml:4:5: Required property i was not initialized")); QQmlComponent comp(&eng); + comp.loadUrl(testFileUrl("createdFromQmlFail.qml")); + QScopedPointer<QObject> obj { comp.create() }; + QVERIFY(obj); + QCOMPARE(qvariant_cast<QQuickItem *>(obj->property("it")), nullptr); + } +} + +struct ComponentWithPublicSetInitial : QQmlComponent +{ + using QQmlComponent::QQmlComponent; + void setInitialProperties(QObject *o, QVariantMap map) + { + QQmlComponent::setInitialProperties(o, map); + } +}; + +void tst_qqmlcomponent::testSetInitialProperties() +{ + QQmlEngine eng; + { + // QVariant + ComponentWithPublicSetInitial comp(&eng); comp.loadUrl(testFileUrl("variantBasedInitialization.qml")); QScopedPointer<QObject> obj { comp.beginCreate(eng.rootContext()) }; QVERIFY(obj); @@ -728,8 +776,6 @@ void tst_qqmlcomponent::testSetInitialProperties() }); #undef ASJSON comp.completeCreate(); - if (!comp.errors().empty()) - qDebug() << comp.errorString() << comp.errors(); QVERIFY(comp.errors().empty()); QCOMPARE(obj->property("i"), 42); QCOMPARE(obj->property("b"), true); @@ -751,13 +797,20 @@ void tst_qqmlcomponent::testSetInitialProperties() } { + // createWithInitialProperties convenience function + QQmlComponent comp(&eng); + comp.loadUrl(testFileUrl("requiredNotSet.qml")); + QScopedPointer<QObject> obj {comp.createWithInitialProperties( QVariantMap { {QLatin1String("i"), QJsonValue{42}} })}; + QVERIFY(obj); + QCOMPARE(obj->property("i"), 42); + } + { // createWithInitialProperties: setting a nonexistent property QQmlComponent comp(&eng); comp.loadUrl(testFileUrl("allJSONTypes.qml")); QScopedPointer<QObject> obj { comp.createWithInitialProperties(QVariantMap { {"notThePropertiesYoureLookingFor", 42} }) }; - qDebug() << comp.errorString(); QVERIFY(obj); QVERIFY(comp.errorString().contains("Could not set property notThePropertiesYoureLookingFor")); } diff --git a/tests/auto/qml/qqmlincubator/data/requiredProperty.qml b/tests/auto/qml/qqmlincubator/data/requiredProperty.qml new file mode 100644 index 0000000000..9e355dce72 --- /dev/null +++ b/tests/auto/qml/qqmlincubator/data/requiredProperty.qml @@ -0,0 +1,5 @@ +import QtQuick 2.12 + +Item { + required property int requiredProperty +} diff --git a/tests/auto/qml/qqmlincubator/tst_qqmlincubator.cpp b/tests/auto/qml/qqmlincubator/tst_qqmlincubator.cpp index 8e25079703..756b3b1d7c 100644 --- a/tests/auto/qml/qqmlincubator/tst_qqmlincubator.cpp +++ b/tests/auto/qml/qqmlincubator/tst_qqmlincubator.cpp @@ -70,6 +70,7 @@ private slots: void selfDelete(); void contextDelete(); void garbageCollection(); + void requiredProperties(); private: QQmlIncubationController controller; @@ -1174,6 +1175,44 @@ void tst_qqmlincubator::garbageCollection() QVERIFY(weakIncubatorRef.isNullOrUndefined()); } +void tst_qqmlincubator::requiredProperties() +{ + { + QQmlComponent component(&engine, testFileUrl("requiredProperty.qml")); + QVERIFY(component.isReady()); + // forceCompletion immediately after creating an asynchronous object completes it + QQmlIncubator incubator; + incubator.setInitialProperties({{"requiredProperty", 42}}); + QVERIFY(incubator.isNull()); + component.create(incubator); + QVERIFY(incubator.isLoading()); + + incubator.forceCompletion(); + + QVERIFY(incubator.isReady()); + QVERIFY(incubator.object() != nullptr); + QCOMPARE(incubator.object()->property("requiredProperty").toInt(), 42); + + delete incubator.object(); + } + { + QQmlComponent component(&engine, testFileUrl("requiredProperty.qml")); + QVERIFY(component.isReady()); + // forceCompletion immediately after creating an asynchronous object completes it + QQmlIncubator incubator; + QVERIFY(incubator.isNull()); + component.create(incubator); + QVERIFY(incubator.isLoading()); + + incubator.forceCompletion(); + + QVERIFY(incubator.isError()); + auto error = incubator.errors().first(); + QVERIFY(error.description().contains(QLatin1String("Required property requiredProperty was not initialized"))); + QVERIFY(incubator.object() == nullptr); + } +} + QTEST_MAIN(tst_qqmlincubator) #include "tst_qqmlincubator.moc" diff --git a/tests/auto/qml/qqmllanguage/data/requiredProperties.1.qml b/tests/auto/qml/qqmllanguage/data/requiredProperties.1.qml new file mode 100644 index 0000000000..dac43c6d88 --- /dev/null +++ b/tests/auto/qml/qqmllanguage/data/requiredProperties.1.qml @@ -0,0 +1,8 @@ +import QtQuick 2.13 +Item { + property var required: 32 // required is still allowed as an identifier for properties + function f(required) { // for javascript + required = required + required; + console.log(required); + } +} diff --git a/tests/auto/qml/qqmllanguage/data/requiredProperties.2.qml b/tests/auto/qml/qqmllanguage/data/requiredProperties.2.qml new file mode 100644 index 0000000000..4c12c7b602 --- /dev/null +++ b/tests/auto/qml/qqmllanguage/data/requiredProperties.2.qml @@ -0,0 +1,4 @@ +import QtQuick 2.13 +Item { + required property int test: 42 // cannot specify value for required property +} diff --git a/tests/auto/qml/qqmllanguage/data/requiredProperties.3.qml b/tests/auto/qml/qqmllanguage/data/requiredProperties.3.qml new file mode 100644 index 0000000000..534322215f --- /dev/null +++ b/tests/auto/qml/qqmllanguage/data/requiredProperties.3.qml @@ -0,0 +1,4 @@ +import QtQuick 2.13 +Item { + default required property int test // cannot have required default property +} diff --git a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp index 8adacd8829..674da19afc 100644 --- a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp +++ b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp @@ -132,6 +132,7 @@ private slots: void autoComponentCreation(); void autoComponentCreationInGroupProperty(); void propertyValueSource(); + void requiredProperty(); void attachedProperties(); void dynamicObjects(); void customVariantTypes(); @@ -1634,6 +1635,25 @@ void tst_qqmllanguage::propertyValueSource() } } +void tst_qqmllanguage::requiredProperty() +{ + QQmlEngine engine; + { + QQmlComponent component(&engine, testFileUrl("requiredProperties.1.qml")); + VERIFY_ERRORS(0); + QScopedPointer<QObject> object(component.create()); + QVERIFY(object); + } + { + QQmlComponent component(&engine, testFileUrl("requiredProperties.2.qml")); + QVERIFY(!component.errors().empty()); + } + { + QQmlComponent component(&engine, testFileUrl("requiredProperties.3.qml")); + QVERIFY(!component.errors().empty()); + } +} + void tst_qqmllanguage::attachedProperties() { QQmlComponent component(&engine, testFileUrl("attachedProperties.qml")); diff --git a/tests/auto/quick/qquickloader/data/RequiredPropertyValuesComponent.qml b/tests/auto/quick/qquickloader/data/RequiredPropertyValuesComponent.qml new file mode 100644 index 0000000000..7bb21e8b93 --- /dev/null +++ b/tests/auto/quick/qquickloader/data/RequiredPropertyValuesComponent.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 + +Item { + id: behaviorCounter + required property int i + required property string s + +} diff --git a/tests/auto/quick/qquickloader/data/initialPropertyValues.10.qml b/tests/auto/quick/qquickloader/data/initialPropertyValues.10.qml new file mode 100644 index 0000000000..4728346ca1 --- /dev/null +++ b/tests/auto/quick/qquickloader/data/initialPropertyValues.10.qml @@ -0,0 +1,20 @@ +import QtQuick 2.0 + +Item { + id: root + property int i: 0 + property string s: "" + + Loader { + id: loader + objectName: "loader" + onLoaded: { + root.i = loader.item.i; // should be 42 + root.s = loader.item.s; // should be 11 + } + } + + Component.onCompleted: { + loader.setSource("RequiredPropertyValuesComponent.qml", {"i": 42}); + } +} diff --git a/tests/auto/quick/qquickloader/data/initialPropertyValues.9.qml b/tests/auto/quick/qquickloader/data/initialPropertyValues.9.qml new file mode 100644 index 0000000000..5d6e3171a0 --- /dev/null +++ b/tests/auto/quick/qquickloader/data/initialPropertyValues.9.qml @@ -0,0 +1,20 @@ +import QtQuick 2.0 + +Item { + id: root + property int i: 0 + property string s: "" + + Loader { + id: loader + objectName: "loader" + onLoaded: { + root.i = loader.item.i; // should be 42 + root.s = loader.item.s; // should be 11 + } + } + + Component.onCompleted: { + loader.setSource("RequiredPropertyValuesComponent.qml", {"i": 42, "s": "hello world"}); + } +} diff --git a/tests/auto/quick/qquickloader/tst_qquickloader.cpp b/tests/auto/quick/qquickloader/tst_qquickloader.cpp index fbdd87905b..da923d4d41 100644 --- a/tests/auto/quick/qquickloader/tst_qquickloader.cpp +++ b/tests/auto/quick/qquickloader/tst_qquickloader.cpp @@ -681,6 +681,16 @@ void tst_QQuickLoader::initialPropertyValues_data() << QStringList() << (QStringList() << "initialValue") << (QVariantList() << 6); + + QTest::newRow("ensure required properties are set correctly") << testFileUrl("initialPropertyValues.9.qml") + << QStringList() + << (QStringList() << "i" << "s") + << (QVariantList() << 42 << QLatin1String("hello world")); + + QTest::newRow("required properties only partially set =") << testFileUrl("initialPropertyValues.10.qml") + << (QStringList() << QString(testFileUrl("RequiredPropertyValuesComponent.qml").toString() + QLatin1String(":6:5: Required property s was not initialized"))) + << (QStringList() << "i" << "s") + << (QVariantList() << 0 << QLatin1String("")); } void tst_QQuickLoader::initialPropertyValues() |