qmltc: Enforce basic required properties

Types in QML can expose "required" properties.

A required property is a property that should be initialized when an
instance of the type is built.

Instantiating a type without initializing its required properties is an
hard error.

Currently, `qmltc` generated types do not respect the semantic of
"required" properties.

Instead, `qmltc` will generally default-initialize a required property
without notifying the user of the error.

`qmtlc`, so as to respect the semantic of required properties, will now
require the user to pass an initial value for all required properties at
construction time.

To do so, `qmltc` will now generate a new inner record,
`RequiredPropertiesBundle`, for each compiled top-level type, that
contains the required amount of data to initialize each top-level
required property that is reachable from the compiled type.

An instance of `RequiredPropertiesBundle` will be required, as long as
the type presents at least one required property, in the user-facing
constructor for the generated type.

The information stored in the instance will later be used to provide an
initial value for each required property during the construction of the
component.

An intermediate representation for `RequiredPropertiesBundle` was added
to "qmltcoutputir.h".
`QmltcCodeWriter`, the component responsible for writing the final C++
code, was modified to take into consideration the presence, or lack
thereof, of a `RequiredPropertiesBundle` and output the necessary code
when required.

The code taking care of populating the various IRs was modified to
populate a `RequiredPropertiesBundle` for top-level components as
necessary.
Similarly, the code populating the parameters of the user-facing
constructor was modified to require an instance of
`RequiredPropertiesBundle` when necessary.

The code that populates the body of the user-facing constructor for
top-level types was modified to make use of the parameter by tying into
the existing structure for setting initial values.

`qmltc` uses a user-provided callback to allow the user to set the
initial values for properties when constructing a top-level component.

The body of the user-facing constructor was modified to compose the
user-provided callback with a callable that sets the initial values for
top-level required properties based on the bundle of data in the new
`RequiredPropertiesBundle` instance.

The code that populates the body of the user-facing constructor was
moved into its own free-function, `compileRootExternalConstructorBody`,
to be slightly more explicit about the structure of the code.

A new test was provided to evaluate, some basic cases for the new
behavior. Some pre-existing tests, which made use of required
properties, were modified to comply with the new generated API.

The documentation for `qmltc` was modified with a note about the new
behavior.

Task-number: QTBUG-120698
Change-Id: I1e916dcd91ae976629dad8adc7eacc6390bce7e9
Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
Reviewed-by: Sami Shalayel <sami.shalayel@qt.io>
This commit is contained in:
Luca Di Sera 2024-03-25 13:13:28 +01:00
parent 6a8b6dbc84
commit fe6f283a5b
10 changed files with 374 additions and 16 deletions

View File

@ -115,6 +115,10 @@ object in C++ is equivalent to creating a new object through
QQmlComponent::create(). Once created, the object could be manipulated from C++
or, for example, combined with QQuickWindow to be drawn on screen.
If a compiled type exposes some required properties, `qmltc` will
require an initial value for those properties in the constructor for
the generated object.
Additionally, the constructor for a qmltc object can be provided with
with a callback to set up initial values for the component's
properties.

View File

@ -23,6 +23,7 @@ set(cpp_sources
cpptypes/typewithnamespace.h cpptypes/typewithnamespace.cpp
cpptypes/typewithsignal.h
cpptypes/custominitialization.h
cpptypes/typewithrequiredproperties.h
)
set(qml_sources
@ -134,6 +135,8 @@ set(qml_sources
NamespacedTypes.qml
badFile.qml
requiredProperties.qml
)
set(js_sources

View File

@ -0,0 +1,75 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtCore/qobject.h>
#include <QtCore/qproperty.h>
#include <QtQuick/qquickitem.h>
#include <QtQml/qqmllist.h>
#include <QtQml/qqmlregistration.h>
#ifndef TYPEWITHREQUIREDPROPERTIES_H_
# define TYPEWITHREQUIREDPROPERTIES_H_
class ExtensionTypeWithRequiredProperties : public QObject
{
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(double requiredPropertyFromExtension READ getRequiredPropertyFromExtension WRITE
setRequiredPropertyFromExtension REQUIRED)
QProperty<double> m_requiredPropertyFromExtension{};
public:
ExtensionTypeWithRequiredProperties(QObject *parent = nullptr) : QObject(parent) { }
double getRequiredPropertyFromExtension() const { return m_requiredPropertyFromExtension; }
void setRequiredPropertyFromExtension(double v) { m_requiredPropertyFromExtension = v; }
};
class TypeWithRequiredProperties : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QQuickItem *inheritedRequiredProperty READ getInheritedRequiredProperty WRITE
setInheritedRequiredProperty REQUIRED)
Q_PROPERTY(int inheritedRequiredPropertyThatWillBeBound READ
getInheritedRequiredPropertyThatWillBeBound WRITE
setInheritedRequiredPropertyThatWillBeBound REQUIRED)
Q_PROPERTY(int nonRequiredInheritedPropertyThatWillBeMarkedRequired READ
getNonRequiredInheritedPropertyThatWillBeMarkedRequired WRITE
setNonRequiredInheritedPropertyThatWillBeMarkedRequired REQUIRED)
QML_EXTENDED(ExtensionTypeWithRequiredProperties)
QProperty<QQuickItem *> m_inheritedRequiredProperty{};
QProperty<int> m_inheritedRequiredPropertyThatWillBeBound{};
QProperty<int> m_nonRequiredInheritedPropertyThatWillBeMarkedRequired{};
public:
TypeWithRequiredProperties(QObject *parent = nullptr) : QObject(parent) { }
QQuickItem *getInheritedRequiredProperty() const { return m_inheritedRequiredProperty; }
void setInheritedRequiredProperty(QQuickItem *v) { m_inheritedRequiredProperty = v; }
int getInheritedRequiredPropertyThatWillBeBound() const
{
return m_inheritedRequiredPropertyThatWillBeBound;
}
void setInheritedRequiredPropertyThatWillBeBound(int v)
{
m_inheritedRequiredPropertyThatWillBeBound = v;
}
int getNonRequiredInheritedPropertyThatWillBeMarkedRequired() const
{
return m_nonRequiredInheritedPropertyThatWillBeMarkedRequired;
}
void setNonRequiredInheritedPropertyThatWillBeMarkedRequired(int v)
{
m_nonRequiredInheritedPropertyThatWillBeMarkedRequired = v;
}
};
#endif // TYPEWITHREQUIREDPROPERTIES_H_

View File

@ -0,0 +1,61 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import QtQuick
import QmltcTests 1.0
TypeWithRequiredProperties {
id: self
required property int primitiveType
required property list<int> valueList
required property list<Item> objectList
property int propertyThatWillBeMarkedRequired
// This is already bound so it should not appear as part of the
// bundle.
inheritedRequiredPropertyThatWillBeBound : 10
// This should be ignored as it alias a required property we are
// already going to consider. It should thus not appear as part of
// the bundle.
property alias aliasToRequiredProperty : self.primitiveType
required propertyThatWillBeMarkedRequired
required nonRequiredInheritedPropertyThatWillBeMarkedRequired
property alias aliasToRequiredInner: inner.requiredInner
// This should be ignored as the underlying property is already bound.
property alias aliasToRequiredBoundedInner: inner.requiredBoundedInner
property alias aliasToInnerThatWillBeMarkedRequired: inner.nonRequiredInner
required aliasToInnerThatWillBeMarkedRequired
property int notRequired
property alias requiredAliasToUnrequiredProperty : self.notRequired
required requiredAliasToUnrequiredProperty
// When we have an alias to a required property in the same scope
// we exclude the alias in favor of setting the property directly.
// See for example aliasToRequiredProperty in this file.
//
// The following alias should instead be picked up, as it point to
// an inner scope.
// Nonetheless, an initial implementation had a bug that would
// discard the alias as long as a property with the same name as
// the target was present in the same scope.
//
// The following alias tests this initially failing case.
property alias aliasToPropertyThatShadows: inner.primitiveType
property Item children : Item {
id: inner
required property int requiredInner
property int nonRequiredInner
required property int requiredBoundedInner
requiredBoundedInner: 43
required property int primitiveType
}
}

View File

@ -90,6 +90,7 @@
#include "qmltablemodel.h"
#include "stringtourl.h"
#include "signalconnections.h"
#include "requiredproperties.h"
// Qt:
#include <QtCore/qstring.h>
@ -202,6 +203,7 @@ void tst_qmltc::initTestCase()
QUrl("qrc:/qt/qml/QmltcTests/calqlatrBits.qml"),
QUrl("qrc:/qt/qml/QmltcTests/valueTypeListProperty.qml"),
QUrl("qrc:/qt/qml/QmltcTests/appendToQQmlListProperty.qml"),
QUrl("qrc:/qt/qml/QmltcTests/requiredProperties.qml"),
};
QQmlEngine e;
@ -845,8 +847,7 @@ void tst_qmltc::customInitialization()
QQmlEngine e;
PREPEND_NAMESPACE(qtbug120700_main)
created(&e, nullptr, [valueToTest, &firstItem, &secondItem](auto& component) {
component.setSomeValue(valueToTest);
created(&e, {valueToTest} ,nullptr, [valueToTest, &firstItem, &secondItem](auto& component) {
component.setSomeComplexValueThatWillBeSet(valueToTest);
component.setPropertyFromExtension(static_cast<double>(valueToTest));
component.setDefaultedBindable(static_cast<double>(valueToTest));
@ -886,6 +887,53 @@ void tst_qmltc::customInitialization()
QCOMPARE(created.getCppObjectList().toList<QList<QQuickItem*>>(), QList({&firstItem, &secondItem}));
}
void tst_qmltc::requiredPropertiesInitialization()
{
QQuickItem item{};
int aliasToInnerThatWillBeMarkedRequired = 10;
int aliasToPropertyThatShadows = 42;
int aliasToRequiredInner = 11;
QQuickItem inheritedRequiredProperty{};
int nonRequiredInheritedPropertyThatWillBeMarkedRequired = 12;
QList<QQuickItem*> objectList{&item};
int primitiveType = 13;
int propertyThatWillBeMarkedRequired = 14;
int requiredAliasToUnrequiredProperty = 15;
double requiredPropertyFromExtension = 16.0;
QList<int> valueList{1, 2, 3, 4};
QQmlEngine e;
PREPEND_NAMESPACE(requiredProperties) created(
&e,
{
aliasToInnerThatWillBeMarkedRequired,
aliasToPropertyThatShadows,
aliasToRequiredInner,
&inheritedRequiredProperty,
nonRequiredInheritedPropertyThatWillBeMarkedRequired,
objectList,
primitiveType,
propertyThatWillBeMarkedRequired,
requiredAliasToUnrequiredProperty,
requiredPropertyFromExtension,
valueList
}
);
QCOMPARE(created.aliasToInnerThatWillBeMarkedRequired(), aliasToInnerThatWillBeMarkedRequired);
QCOMPARE(created.aliasToPropertyThatShadows(), aliasToPropertyThatShadows);
QCOMPARE(created.aliasToRequiredInner(), aliasToRequiredInner);
QCOMPARE(created.getInheritedRequiredProperty(), &inheritedRequiredProperty);
QCOMPARE(created.getNonRequiredInheritedPropertyThatWillBeMarkedRequired(), nonRequiredInheritedPropertyThatWillBeMarkedRequired);
QCOMPARE(created.objectList().toList<QList<QQuickItem*>>(), objectList);
QCOMPARE(created.primitiveType(), primitiveType);
QCOMPARE(created.propertyThatWillBeMarkedRequired(), propertyThatWillBeMarkedRequired);
QCOMPARE(created.requiredAliasToUnrequiredProperty(), requiredAliasToUnrequiredProperty);
QCOMPARE(created.property("requiredPropertyFromExtension").toDouble(), requiredPropertyFromExtension);
QCOMPARE(created.valueList(), valueList);
}
// QTBUG-104094
void tst_qmltc::nonStandardIncludesInsideModule()
{
@ -1132,7 +1180,7 @@ void tst_qmltc::propertyAlias_external()
void tst_qmltc::propertyAliasAttribute()
{
QQmlEngine e;
PREPEND_NAMESPACE(propertyAliasAttributes) fromQmltc(&e);
PREPEND_NAMESPACE(propertyAliasAttributes) fromQmltc(&e, {""});
QQmlComponent c(&e);
c.loadUrl(QUrl("qrc:/qt/qml/QmltcTests/propertyAliasAttributes.qml"));

View File

@ -36,6 +36,7 @@ private slots:
void extensionTypeBindings();
void visibleAliasMethods(); // QTBUG-103956
void customInitialization(); // QTBUG-120700
void requiredPropertiesInitialization();
void nonStandardIncludesInsideModule(); // QTBUG-104094
void specialProperties();
void regexpBindings();

View File

@ -203,6 +203,22 @@ void QmltcCodeWriter::write(QmltcOutputWrapper &code,
code.rawAppendToHeader(u""); // blank line
}
void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcRequiredPropertiesBundle &requiredPropertiesBundle)
{
code.rawAppendToHeader(u"struct " + requiredPropertiesBundle.name + u" {");
{
[[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code);
for (const auto &member : requiredPropertiesBundle.members) {
write(code, member);
}
}
code.rawAppendToHeader(u"};"_s);
code.rawAppendToHeader(u""); // blank line
}
void QmltcCodeWriter::writeGlobalFooter(QmltcOutputWrapper &code, const QString &sourcePath,
const QString &outNamespace)
{
@ -339,6 +355,9 @@ void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcType &type,
if (!type.propertyInitializer.name.isEmpty())
write(code, type.propertyInitializer, type);
if (type.requiredPropertiesBundle)
write(code, *type.requiredPropertiesBundle);
// NB: when non-document root, the externalCtor won't be public - but we
// really don't care about the output format of such types
if (!type.ignoreInit && type.externalCtor.access == QQmlJSMetaMethod::Public) {

View File

@ -28,6 +28,7 @@ struct QmltcCodeWriter
static void write(QmltcOutputWrapper &code, const QmltcVariable &var);
static void write(QmltcOutputWrapper &code, const QmltcProperty &prop);
static void write(QmltcOutputWrapper &code, const QmltcPropertyInitializer &propertyInitializer, const QmltcType& wrappedType);
static void write(QmltcOutputWrapper &code, const QmltcRequiredPropertiesBundle &requiredPropertiesBundle);
private:
static void writeUrl(QmltcOutputWrapper &code, const QmltcMethod &urlMethod); // special

View File

@ -23,6 +23,137 @@ bool qIsReferenceTypeList(const QQmlJSMetaProperty &p)
return false;
}
static QList<QQmlJSMetaProperty> unboundRequiredProperties(
const QQmlJSScope::ConstPtr &type,
QmltcTypeResolver *resolver
) {
QList<QQmlJSMetaProperty> requiredProperties{};
auto isPropertyRequired = [&type, &resolver](const auto &property) {
if (!type->isPropertyRequired(property.propertyName()))
return false;
if (type->hasPropertyBindings(property.propertyName()))
return false;
if (property.isAlias()) {
QQmlJSUtils::AliasResolutionVisitor aliasVisitor;
QQmlJSUtils::ResolvedAlias result =
QQmlJSUtils::resolveAlias(resolver, property, type, aliasVisitor);
if (result.kind != QQmlJSUtils::AliasTarget_Property)
return false;
// If the top level alias targets a property that is in
// the top level scope and that property is required, then
// we will already pick up the property during one of the
// iterations.
// Setting the property or the alias is the same so we
// discard one of the two, as otherwise we would require
// the user to pass two values for the same property ,in
// this case the alias.
//
// For example in:
//
// ```
// Item {
// id: self
// required property int foo
// property alias bar: self.foo
// }
// ```
//
// Both foo and bar are required but setting one or the
// other is the same operation so that we should choose
// only one.
if (result.owner == type &&
type->isPropertyRequired(result.property.propertyName()))
return false;
if (result.owner->hasPropertyBindings(result.property.propertyName()))
return false;
}
return true;
};
const auto properties = type->properties();
std::copy_if(properties.cbegin(), properties.cend(),
std::back_inserter(requiredProperties), isPropertyRequired);
std::sort(requiredProperties.begin(), requiredProperties.end(),
[](const auto &left, const auto &right) {
return left.propertyName() < right.propertyName();
});
return requiredProperties;
}
// Populates the internal representation for a
// RequiredPropertiesBundle, a class that acts as a bundle of initial
// values that should be set for the required properties of a type.
static void compileRequiredPropertiesBundle(
QmltcType &current,
const QQmlJSScope::ConstPtr &type,
QmltcTypeResolver *resolver
) {
QList<QQmlJSMetaProperty> requiredProperties = unboundRequiredProperties(type, resolver);
if (requiredProperties.isEmpty())
return;
current.requiredPropertiesBundle.emplace();
current.requiredPropertiesBundle->name = u"RequiredPropertiesBundle"_s;
current.requiredPropertiesBundle->members.reserve(requiredProperties.size());
std::transform(requiredProperties.cbegin(), requiredProperties.cend(),
std::back_inserter(current.requiredPropertiesBundle->members),
[](const QQmlJSMetaProperty &property) {
QString type = qIsReferenceTypeList(property)
? u"const QList<%1*>&"_s.arg(
property.type()->valueType()->internalName())
: u"passByConstRefOrValue<%1>"_s.arg(
property.type()->augmentedInternalName());
return QmltcVariable{ type, property.propertyName() };
});
}
static void compileRootExternalConstructorBody(
QmltcType& current,
const QQmlJSScope::ConstPtr &type
) {
current.externalCtor.body << u"// document root:"_s;
// if it's document root, we want to create our QQmltcObjectCreationBase
// that would store all the created objects
current.externalCtor.body << u"QQmltcObjectCreationBase<%1> objectHolder;"_s.arg(
type->internalName());
current.externalCtor.body
<< u"QQmltcObjectCreationHelper creator = objectHolder.view();"_s;
current.externalCtor.body << u"creator.set(0, this);"_s; // special case
QString initializerName = u"initializer"_s;
if (current.requiredPropertiesBundle) {
// Compose new initializer based on the initial values for required properties.
current.externalCtor.body << u"auto newInitializer = [&](auto& propertyInitializer) {"_s;
for (const auto& member : current.requiredPropertiesBundle->members) {
current.externalCtor.body << u" propertyInitializer.%1(requiredPropertiesBundle.%2);"_s.arg(
QmltcPropertyData(member.name).write, member.name
);
}
current.externalCtor.body << u" initializer(propertyInitializer);"_s;
current.externalCtor.body << u"};"_s;
initializerName = u"newInitializer"_s;
}
// now call init
current.externalCtor.body << current.init.name
+ u"(&creator, engine, QQmlContextData::get(engine->rootContext()), /* "
u"endInit */ true, %1);"_s.arg(initializerName);
};
Q_LOGGING_CATEGORY(lcQmltcCompiler, "qml.qmltc.compiler", QtWarningMsg);
const QString QmltcCodeGenerator::privateEngineName = u"ePriv"_s;
@ -299,11 +430,22 @@ void QmltcCompiler::compileType(
u"initializer"_s,
u"[](%1&){}"_s.arg(current.propertyInitializer.name));
current.externalCtor.parameterList = { engine, parent, initializer };
current.init.parameterList = { creator, engine, ctxtdata, finalizeFlag, initializer };
current.beginClass.parameterList = { creator, finalizeFlag };
current.completeComponent.parameterList = { creator, finalizeFlag };
current.finalizeComponent.parameterList = { creator, finalizeFlag };
compileRequiredPropertiesBundle(current, type, m_typeResolver);
if (current.requiredPropertiesBundle) {
QmltcVariable bundle{
u"const %1&"_s.arg(current.requiredPropertiesBundle->name),
u"requiredPropertiesBundle"_s,
};
current.externalCtor.parameterList = { engine, bundle, parent, initializer };
} else {
current.externalCtor.parameterList = { engine, parent, initializer };
}
} else {
current.externalCtor.parameterList = { creator, engine, parent };
current.init.parameterList = { creator, engine, ctxtdata };
@ -327,18 +469,7 @@ void QmltcCompiler::compileType(
// compilation stub:
current.externalCtor.body << u"Q_UNUSED(engine)"_s;
if (documentRoot || inlineComponent) {
current.externalCtor.body << u"// document root:"_s;
// if it's document root, we want to create our QQmltcObjectCreationBase
// that would store all the created objects
current.externalCtor.body << u"QQmltcObjectCreationBase<%1> objectHolder;"_s.arg(
type->internalName());
current.externalCtor.body
<< u"QQmltcObjectCreationHelper creator = objectHolder.view();"_s;
current.externalCtor.body << u"creator.set(0, this);"_s; // special case
// now call init
current.externalCtor.body << current.init.name
+ u"(&creator, engine, QQmlContextData::get(engine->rootContext()), /* "
u"endInit */ true, initializer);";
compileRootExternalConstructorBody(current, type);
} else {
current.externalCtor.body << u"// not document root:"_s;
// just call init, we don't do any setup here otherwise

View File

@ -115,6 +115,19 @@ struct QmltcPropertyInitializer {
QList<QmltcMethod> propertySetters;
};
// Represents a generated class that contains a bundle of values to
// initialize the required properties of a type.
//
// This is generally intended to be available for the root component
// of the document, where it will be used as a constructor argument to
// force the user to provide initial values for the required
// properties of the constructed type.
struct QmltcRequiredPropertiesBundle {
QString name;
QList<QmltcVariable> members;
};
// Represents QML -> C++ compiled type
struct QmltcType
{
@ -158,6 +171,8 @@ struct QmltcType
// A proxy class that provides a restricted interface that only
// allows setting the properties of the type.
QmltcPropertyInitializer propertyInitializer{};
std::optional<QmltcRequiredPropertiesBundle> requiredPropertiesBundle{};
};
// Represents whole QML program, compiled to C++