Revision control

Copy as Markdown

// Copyright (C) 2026 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
// Qt-Security score:significant reason:default
#include "qqmlpreviewbindingpatchcontext_p.h"
#include <private/qqmlcomponent_p.h>
#include <private/qqmlnotifier_p.h>
#include <private/qqmlobjectcreator_p.h>
#include <private/qqmlproperty_p.h>
#include <private/qqmlproperty_p.h>
#include <private/qqmlpropertybinding_p.h>
#include <private/qqmlpropertytopropertybinding_p.h>
#include <private/qqmltypeloader_p.h>
#include <private/qqmlvaluetypeproxybinding_p.h>
#include <private/qqmlvme_p.h>
#include <private/qv4functionobject_p.h>
#include <private/qv4generatorobject_p.h>
#include <private/qv4qmlcontext_p.h>
#include <private/qv4resolvedtypereference_p.h>
#include <QtCore/qqueue.h>
#include <QtCore/qset.h>
QT_BEGIN_NAMESPACE
namespace QQmlPreview {
static bool functionBelongsToObject(const QV4::Function *f,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &cu,
int objectIndex)
{
if (f->executableCompilationUnit() != cu)
return false;
const QV4::CompiledData::Object *obj = cu->objectAt(objectIndex);
for (auto binding = obj->bindingsBegin(), end = obj->bindingsEnd(); binding != end; ++binding) {
switch (binding->type()) {
case QV4::CompiledData::Binding::Type_GroupedProperty:
case QV4::CompiledData::Binding::Type_AttachedProperty:
case QV4::CompiledData::Binding::Type_Object:
if (functionBelongsToObject(f, cu, binding->value.objectIndex))
return true;
break;
case QV4::CompiledData::Binding::Type_Script:
if (cu->runtimeFunctions[binding->value.compiledScriptIndex] == f)
return true;
default:
break;
}
}
return false;
}
// A binding that lives in one of target's outer contexts is recreated when that context's root is
// rebuilt, so it is not actually external. Returns true if `cu` is reachable through the context
// chain before we leave the set of units being rebuilt.
static bool rebuildReachesUnitViaContext(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &cu,
const std::vector<CompositeLevel> &internalUnits,
QObject *target)
{
const QQmlData *ddata = QQmlData::get(target);
if (!ddata)
return false;
for (QQmlRefPointer<QQmlContextData> context = ddata->outerContext; context;
context = context->parent()) {
const QQmlRefPointer<QV4::ExecutableCompilationUnit> ctxCu = context->typeCompilationUnit();
if (!ctxCu)
continue;
if (ctxCu == cu)
return true;
if (std::any_of(internalUnits.begin(), internalUnits.end(),
[&](const CompositeLevel &level) {
return level.oldCu == ctxCu || level.newCu == ctxCu;
})) {
break;
}
}
return false;
}
// Classifies a single binding's JavaScript function: does it belong to one of the compilation
// units participating in the rebuild (internal, will be recreated), or to some other component
// (external, must be preserved)? A null function is treated as external.
static bool isExternalFunction(const QV4::Function *f,
const std::vector<CompositeLevel> &internalUnits, QObject *target)
{
if (!f)
return true;
for (const auto &internalUnit : internalUnits) {
if (functionBelongsToObject(f, internalUnit.oldCu, internalUnit.objectIndex)
|| functionBelongsToObject(f, internalUnit.newCu, internalUnit.objectIndex)) {
return false;
}
}
return !rebuildReachesUnitViaContext(f->executableCompilationUnit(), internalUnits, target);
}
// A translation binding has no JavaScript function, so it cannot be classified via
// isExternalFunction. Classify it by its compilation unit instead: it is internal if that unit is
// one of the units being rebuilt, directly or through the context chain.
static bool isExternalUnitBinding(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &cu,
const std::vector<CompositeLevel> &internalUnits, QObject *target)
{
if (!cu)
return true;
for (const auto &internalUnit : internalUnits) {
if (internalUnit.oldCu == cu || internalUnit.newCu == cu)
return false;
}
return !rebuildReachesUnitViaContext(cu, internalUnits, target);
}
// Determines whether a binding on a property is "external", i.e. not from any of the
// compilation units that participate in the rebuild of this object.
// External bindings come from other compilation units (e.g. a parent component setting a
// property binding on a child instance) and must be preserved across rebuilds.
static bool isExternalBinding(const QQmlAnyBinding &binding,
const std::vector<CompositeLevel> &internalUnits, QObject *target)
{
if (!binding)
return false;
if (const QQmlAbstractBinding *abstractBinding = binding.asAbstractBinding()) {
switch (abstractBinding->kind()) {
case QQmlAbstractBinding::QmlBinding: {
const auto *qmlBinding = static_cast<const QQmlBinding *>(abstractBinding);
if (const QV4::Function *f = qmlBinding->function())
return isExternalFunction(f, internalUnits, target);
// No function: this may be a translation binding. Classify it by its compilation unit.
return isExternalUnitBinding(qmlBinding->compilationUnit(), internalUnits, target);
}
case QQmlAbstractBinding::ValueTypeProxy: {
// A value-type group binding has no function of its own: it is a proxy holding one
// QQmlBinding per bound sub-property. Classify it by its sub-bindings.
const auto *proxy = static_cast<const QQmlValueTypeProxyBinding *>(abstractBinding);
for (QQmlAbstractBinding *sub = proxy->subBindings(); sub; sub = sub->nextBinding()) {
if (sub->kind() != QQmlAbstractBinding::QmlBinding)
continue;
if (!isExternalFunction(static_cast<const QQmlBinding *>(sub)->function(),
internalUnits, target)) {
return false;
}
}
return true;
}
case QQmlAbstractBinding::PropertyToPropertyBinding:
return true;
}
return true;
}
if (const QPropertyBindingPrivate *priv =
QPropertyBindingPrivate::get(binding.asUntypedPropertyBinding())) {
if (priv->isQmlBinding()) {
// Check if it's a QQmlPropertyBinding with a JS expression we can trace back to a CU.
const auto base = static_cast<const QQmlPropertyBindingBase *>(priv);
if (base->bindingKind() == QQmlPropertyBindingBase::BindingKind::JavaScript) {
if (const QQmlPropertyBindingJS *jsExpr =
static_cast<const QQmlPropertyBinding *>(base)->jsExpression()) {
return isExternalFunction(jsExpr->function(), internalUnits, target);
}
}
} else if (const auto cu = QQmlTranslationPropertyBinding::compilationUnit(priv)) {
// A translation binding on a bindable property is a plain QUntypedPropertyBinding
// rather than a QQmlPropertyBinding. Recover its compilation unit to classify it like
// the QQmlBinding case.
return isExternalUnitBinding(cu, internalUnits, target);
}
}
return true;
}
static QObject *propertyToPropertySource(const QQmlAnyBinding &binding)
{
if (const QQmlAbstractBinding *abstractBinding = binding.asAbstractBinding()) {
if (abstractBinding->kind() == QQmlAbstractBinding::PropertyToPropertyBinding) {
return static_cast<const QQmlPropertyToUnbindablePropertyBinding *>(abstractBinding)
->source();
}
} else if (const QPropertyBindingPrivate *priv =
QPropertyBindingPrivate::get(binding.asUntypedPropertyBinding());
priv && priv->isQmlBinding()) {
const auto base = static_cast<const QQmlPropertyBindingBase *>(priv);
if (base->bindingKind() == QQmlPropertyBindingBase::BindingKind::PropertyToProperty)
return static_cast<const QQmlPropertyToBindablePropertyBinding *>(priv)->source();
}
return nullptr;
}
static QVariant literalBindingValue(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &unit,
const QV4::CompiledData::Binding *binding)
{
switch (binding->type()) {
case QV4::CompiledData::Binding::Type_Number: {
const double d = unit->bindingValueAsNumber(binding);
return QV4::Value::isInt32(d) ? QVariant(int(d)) : QVariant(d);
}
case QV4::CompiledData::Binding::Type_Boolean:
return QVariant(bool(binding->value.b));
case QV4::CompiledData::Binding::Type_Translation:
case QV4::CompiledData::Binding::Type_TranslationById:
case QV4::CompiledData::Binding::Type_String:
return unit->bindingValueAsString(binding);
case QV4::CompiledData::Binding::Type_Null:
return QVariant::fromValue(nullptr);
default:
// Script bindings, object bindings, etc. carry no constant value.
return QVariant();
}
}
static QVariant coerceToPropertyType(QV4::ExecutionEngine *v4, const QVariant &value,
QMetaType propertyType)
{
if (propertyType == QMetaType::fromType<QVariant>() || value.metaType() == propertyType)
return value;
QV4::Scope scope(v4);
QV4::ScopedValue v(scope, v4->metaTypeToJS(value.metaType(), value.constData()));
QVariant result(propertyType);
v4->metaTypeFromJS(v, propertyType, result.data());
return result;
}
void BindingPatchContext::recordBindingValues(
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &unit, int cuIndex,
QHash<QString, QVariant> *constantValues, QDuplicateTracker<QObject *> *seenChildren)
{
Q_ASSERT(constantValues);
if (!unit || cuIndex >= unit->objectCount())
return;
const QV4::CompiledData::Object *obj = unit->objectAt(cuIndex);
const QQmlPropertyCache::ConstPtr cache = unit->propertyCachesPtr()->at(cuIndex);
for (auto binding = obj->bindingsBegin(), end = obj->bindingsEnd(); binding != end; ++binding) {
const QString name = targetPropertyName(unit, cuIndex, binding);
if (name.isEmpty())
continue;
switch (binding->type()) {
case QV4::CompiledData::Binding::Type_AttachedProperty:
attachedContext(unit, binding, seenChildren);
continue;
case QV4::CompiledData::Binding::Type_GroupedProperty:
childContext(unit, cuIndex, binding, seenChildren);
continue;
default:
break;
}
if (unit->bindingIsSignalHandler(cuIndex, int(binding - obj->bindingsBegin())))
continue;
if (unit->bindingIsCustomParser(cuIndex, int(binding - obj->bindingsBegin())))
continue;
const qsizetype size = constantValues->size();
QVariant &value = (*constantValues)[name];
if (constantValues->size() == size)
continue;
// Extract constant value from the binding for comparison. Script and object bindings
// yield an invalid QVariant: they shouldn't be touched since we've just installed the
// new bindings (unless there is yet another, external binding).
value = literalBindingValue(unit, binding);
}
for (int propertyIndex = 0, end = obj->propertyCount(); propertyIndex != end; ++propertyIndex) {
const qsizetype size = constantValues->size();
QVariant &value =
(*constantValues)[unit->stringAt(obj->propertyTable()[propertyIndex].nameIndex())];
if (constantValues->size() != size)
value = QVariant(cache->property(cache->propertyOffset() + propertyIndex)->propType());
}
}
void BindingPatchContext::stashExternalState(const std::vector<CompositeLevel> &internalUnits,
QDuplicateTracker<QObject *> *seenChildren)
{
// Determine which properties are assigned by the CU and their constant values
QHash<QString, QVariant> constantValues;
recordBindingValues(unit, objectIndex, &constantValues, seenChildren);
if (prefix.isEmpty()) {
const QQmlData *ddata = QQmlData::get(m_object);
if (ddata->compilationUnit) {
recordBindingValues(ddata->compilationUnit, ddata->cuObjectIndex, &constantValues,
seenChildren);
}
if (ddata->hasVMEMetaObject) {
for (QQmlVMEMetaObject *vmeMeta = static_cast<QQmlVMEMetaObject *>(
QObjectPrivate::get(m_object)->metaObject);
vmeMeta; vmeMeta = vmeMeta->parentVMEMetaObject()) {
if (auto cu = vmeMeta->compilationUnit())
recordBindingValues(cu, vmeMeta->qmlObjectId(), &constantValues, seenChildren);
}
}
}
// Iterate all properties. For those in the CU's binding table, check if the current state
// differs from what the CU set (indicating an external override to preserve). For the rest,
// check for external bindings installed by other components.
// Additionally, for QObject* properties pointing to QML-created children, register them
// as child contexts so their external signal handlers are stashed recursively at the end.
const QMetaObject *mo = m_object->metaObject();
for (int i = 0, count = mo->propertyCount(); i < count; ++i) {
const QMetaProperty metaProp = mo->property(i);
const QString propName = QString::fromUtf8(metaProp.name());
const QQmlProperty qProp(m_object, propName);
if (!qProp.isValid())
continue;
// Discover QML-created child objects accessible via QObject* properties.
// Objects without a CU (like lazily-created grouped property objects) survive
// rebuilds unchanged and don't need stashing.
if (qProp.propertyMetaType().flags().testFlag(QMetaType::PointerToQObject)) {
if (QObject *child = qProp.read().value<QObject *>()) {
if (QQmlData *childDdata = QQmlData::get(child)) {
if (const auto &childCU = childDdata->compilationUnit; childCU
&& std::find_if(internalUnits.begin(), internalUnits.end(),
[&](const CompositeLevel &level) {
return level.newCu == childCU || level.oldCu == childCU;
})
!= internalUnits.end()) {
childContext(propName, child, childCU, childDdata->cuObjectIndex,
seenChildren);
}
}
}
}
const auto it = constantValues.constFind(propName);
if (it == constantValues.cend()) {
// Property not in CU's binding table — check for external bindings.
const QQmlAnyBinding binding = QQmlAnyBinding::ofProperty(qProp);
if (isExternalBinding(binding, internalUnits, m_object)) {
QQmlAnyBinding taken = QQmlAnyBinding::takeFrom(qProp);
m_storedBindings.push_back(
{ propName, std::move(taken), propertyToPropertySource(binding) });
}
continue;
}
// Property is in the CU's binding table.
const QQmlAnyBinding binding = QQmlAnyBinding::ofProperty(qProp);
if (isExternalBinding(binding, internalUnits, m_object)) {
QQmlAnyBinding taken = QQmlAnyBinding::takeFrom(qProp);
m_storedBindings.push_back(
{ propName, std::move(taken), propertyToPropertySource(binding) });
continue;
}
// Internal binding is still valid. Apparently it doesn't get overridden by an external
// constant or binding. Nothing to store.
if (binding)
continue;
if (!it->isValid()) {
// This is potentially an internal binding overridden by an external constant. But
// it can also be an enum assignment optimized away to omit the binding itself. We
// can't discern those. So we don't store them for now.
// TODO: We can probably do better here.
continue;
}
// Two constant values. Figure out if they're the same. If not, store.
const QVariant expected = coerceToPropertyType(unit->engine, *it, qProp.propertyMetaType());
if (const QVariant current = qProp.read(); current != expected)
m_storedValues.push_back({ propName, current });
}
const auto stashBoundSignal = [&](QQmlBoundSignal *boundSignal) {
const QByteArray signature =
QMetaObjectPrivate::signal(m_object->metaObject(), boundSignal->signalIndex())
.methodSignature();
QQmlNotifierEndpoint *next = boundSignal->nextEndpoint();
boundSignal->disconnect();
m_storedSignalHandlers.push_back(
{ QString::fromUtf8(signature), std::unique_ptr<QQmlBoundSignal>(boundSignal) });
return next;
};
// Stash external signal handlers connected to this object's signals.
// A handler is "internal" only if its function will be recreated during repopulation
// (i.e., it's a signal handler binding at one of the specific object indices being rebuilt).
// Only QQmlBoundSignal endpoints are stashed — other notifier endpoints (e.g. alias
// tracking) are embedded in VME data arrays and cannot be safely owned or relocated.
if (QQmlNotifyList *list = QQmlData::get(m_object)->notifyList.loadRelaxed()) {
// Ensure all endpoints are moved from the pending 'todo' list into the
// laid-out 'notifies' array. Endpoints remain in 'todo' until a signal
// with a high enough index is actually delivered, so without this call
// we'd miss handlers for signals that were never fired (e.g. clicked()).
if (list->todo)
list->layout();
for (quint16 i = 0, end = list->notifiesSize; i < end; ++i) {
for (QQmlNotifierEndpoint *ep = list->notifies[i]; ep;) {
if (ep->callbackType() != QQmlNotifierEndpoint::QQmlBoundSignal) {
ep = ep->nextEndpoint();
continue;
}
QQmlBoundSignal *boundSignal = static_cast<QQmlBoundSignal *>(ep);
QQmlBoundSignalExpression *expr = boundSignal->expression();
if (!expr) {
ep = stashBoundSignal(boundSignal);
continue;
}
const QV4::Function *f = expr->function();
if (!f) {
ep = stashBoundSignal(boundSignal);
continue;
}
bool isInternal = false;
for (const CompositeLevel &internalUnit : internalUnits) {
if (functionBelongsToObject(f, internalUnit.oldCu, internalUnit.objectIndex)
|| functionBelongsToObject(f, internalUnit.newCu,
internalUnit.objectIndex)) {
isInternal = true;
break;
}
}
ep = isInternal ? ep->nextEndpoint() : stashBoundSignal(boundSignal);
}
}
}
// Recurse into child contexts (grouped properties)
for (auto &[name, child] : m_children) {
if (child)
child->stashExternalState(internalUnits, seenChildren);
}
}
void BindingPatchContext::refreshObjects()
{
if (!m_object)
return;
// After a rebuild, child objects (accessed via grouped properties) may have
// been replaced. Re-fetch QObject pointers from the parent's properties so
// that restoreExternalState() reconnects to the new objects.
for (auto &[name, child] : m_children) {
if (!child)
continue;
// Children with a non-empty prefix share m_object with their parent
// (value-type grouped properties like "font."). Update them to match.
if (!child->prefix.isEmpty()) {
child->m_object = m_object;
child->refreshObjects();
continue;
}
if (QObject *newObj = m_object->property(name.toUtf8()).value<QObject *>())
child->m_object = newObj;
child->refreshObjects();
}
}
void BindingPatchContext::restoreExternalState()
{
if (!m_object)
return;
// Restore external bindings (look up by name since indices may have shifted)
for (auto &stored : m_storedBindings) {
if (!stored.binding)
continue;
// If this was a property-to-property binding, verify the source object survived the
// rebuild. Delegate model items (the source for required-property bindings) are
// destroyed when the delegate is re-instantiated and new bindings are created
// automatically. Restoring a stale binding would dereference freed memory.
if (stored.sourceGuard.isNull()) {
bool isPTP = false;
if (auto *abstractBinding = stored.binding.asAbstractBinding()) {
isPTP = abstractBinding->kind()
== QQmlAbstractBinding::PropertyToPropertyBinding;
} else if (const QPropertyBindingPrivate *priv = QPropertyBindingPrivate::get(
stored.binding.asUntypedPropertyBinding())) {
const auto base = static_cast<const QQmlPropertyBindingBase *>(priv);
isPTP = base->bindingKind()
== QQmlPropertyBindingBase::BindingKind::PropertyToProperty;
}
if (isPTP)
continue;
}
QQmlProperty qProp(m_object, stored.propertyName);
if (!qProp.isValid())
continue;
// After a rebuild, child objects may have been replaced (refreshObjects).
// The stashed binding's targetObject still references the old object.
// Update it to the new object before installing, otherwise installOn()
// asserts that targetObject() == target.object().
if (auto *abstractBinding = stored.binding.asAbstractBinding()) {
if (abstractBinding->targetObject() != qProp.object())
abstractBinding->setTarget(qProp);
}
stored.binding.installOn(qProp);
}
m_storedBindings.clear();
// Restore externally set values (only if no new binding was installed)
for (auto &stored : m_storedValues) {
const QMetaObject *mo = m_object->metaObject();
const int idx = mo->indexOfProperty(stored.propertyName.toUtf8().constData());
if (idx < 0)
continue;
// Don't overwrite if a binding was just installed by repopulateBindings
QQmlProperty qProp(m_object, stored.propertyName);
if (!qProp.isValid())
continue;
QQmlAnyBinding currentBinding = QQmlAnyBinding::ofProperty(qProp);
if (currentBinding)
continue;
mo->property(idx).write(m_object, stored.value);
}
m_storedValues.clear();
// Restore external signal handlers that were detached during stash.
// Reconnect them to this object's signals.
if (!m_storedSignalHandlers.empty()) {
QQmlEngine *engine = unit->engine->qmlEngine();
for (auto &stored : m_storedSignalHandlers) {
const QMetaObject *metaObject = m_object->metaObject();
const int signalIndex = QMetaObjectPrivate::signalIndex(
metaObject->method(metaObject->indexOfSignal(stored.signature.toUtf8())));
if (signalIndex >= 0)
QQmlData::connectEndpoint(stored.handler.release(), m_object, signalIndex, engine);
}
}
m_storedSignalHandlers.clear();
// Recurse into child contexts (grouped properties)
for (auto &[name, child] : m_children) {
if (child)
child->restoreExternalState();
}
}
// The object referenced by the id "name" in the context the object belongs to, or nullptr if
// there is no such id. Used to resolve the first chain part of a generalized grouped property
// ("someId.x": here name == "someId").
static QObject *idTarget(QObject *object, const QString &name)
{
const QQmlData *ddata = QQmlData::get(object);
Q_ASSERT(ddata);
QQmlContextData *context = ddata->ownContext ? ddata->ownContext.data() : ddata->context;
Q_ASSERT(context);
return context->asQQmlContext()->objectForName(name);
}
BindingPatchContext *
BindingPatchContext::childContext(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &unit,
int objectIndex, const QV4::CompiledData::Binding *binding,
QDuplicateTracker<QObject *> *seenChildren)
{
const QString name = unit->stringAt(binding->propertyNameIndex);
const size_t size = m_children.size();
std::unique_ptr<BindingPatchContext> &child = m_children[name];
if (size == m_children.size())
return child.get();
if (!seenChildren)
return nullptr;
const QByteArray nameUtf8 = name.toUtf8();
QObject *groupObject = nullptr;
if (m_object->metaObject()->indexOfProperty(nameUtf8.constData()) >= 0) {
// "name" is a property of m_object, so it always wins over an id of the same name. A
// QObject-valued property recurses into that object; a value-type property
// ("font.pixelSize") uses a prefix on m_object (target stays null).
groupObject = m_object->property(nameUtf8).value<QObject *>();
} else if (unit->bindingIsDeferred(
objectIndex, int(binding - unit->objectAt(objectIndex)->bindingTable()))) {
// Not a property of m_object: a deferred group binding is a generalized grouped
// property whose first chain part is an id ("someId.x"), targeting an external object.
//
// This is a deferred property, so it's not actually guaranteed to do what we think
// it does. However, if we actually find the expected object and if we can patch its
// property, that is it's current value is the one we'd expect from the old binding, we
// assume we're guessing right.
groupObject = idTarget(m_object, name);
}
if (groupObject) {
if (seenChildren->hasSeen(groupObject))
return nullptr;
child = std::make_unique<BindingPatchContext>(groupObject, unit,
binding->value.objectIndex);
} else {
child = std::make_unique<BindingPatchContext>(m_object, unit, binding->value.objectIndex,
name);
}
return child.get();
}
BindingPatchContext *
BindingPatchContext::childContext(const QString &name, QObject *object,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &unit,
int objectIndex, QDuplicateTracker<QObject *> *seenChildren)
{
const size_t size = m_children.size();
std::unique_ptr<BindingPatchContext> &child = m_children[name];
if (size == m_children.size()) {
Q_ASSERT(!child || child->m_object == object);
return child.get();
}
if (!seenChildren || seenChildren->hasSeen(object))
return nullptr;
child = std::make_unique<BindingPatchContext>(object, unit, objectIndex);
return child.get();
}
BindingPatchContext *
BindingPatchContext::attachedContext(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &unit,
const QV4::CompiledData::Binding *binding,
QDuplicateTracker<QObject *> *seenChildren)
{
const QString name = unit->stringAt(binding->propertyNameIndex);
const size_t size = m_children.size();
std::unique_ptr<BindingPatchContext> &child = m_children[name];
if (size == m_children.size())
return child.get();
if (!seenChildren)
return nullptr;
QV4::ResolvedTypeReference *typeRef = unit->resolvedType(binding->propertyNameIndex);
Q_ASSERT(typeRef);
QQmlAttachedPropertiesFunc func =
typeRef->type().attachedPropertiesFunction(unit->engine->typeLoader());
Q_ASSERT(func);
if (QObject *attached = QQmlData::get(m_object)->attachedProperties()->value(func)) {
if (seenChildren->hasSeen(attached))
return nullptr;
child = std::make_unique<BindingPatchContext>(attached, unit, binding->value.objectIndex);
}
return child.get();
}
bool BindingPatchContext::applyBindingChange(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit,
const QV4::CompiledData::Change &change)
{
Q_ASSERT(change.type == QV4::CompiledData::ChangeType::BindingChanged);
if (!m_object || objectIndex < 0 || objectIndex >= unit->objectCount())
return false;
if (objectIndex == change.objectIndex) {
patchBinding(newUnit, change);
return true;
}
QDuplicateTracker<QObject *> seenChildren;
const QV4::CompiledData::Object *obj = unit->objectAt(objectIndex);
for (auto binding = obj->bindingsBegin(), end = obj->bindingsEnd(); binding != end; ++binding) {
BindingPatchContext *child = nullptr;
switch (binding->type()) {
case QV4::CompiledData::Binding::Type_GroupedProperty:
child = childContext(unit, objectIndex, binding, &seenChildren);
break;
case QV4::CompiledData::Binding::Type_AttachedProperty:
child = attachedContext(unit, binding, &seenChildren);
break;
default:
continue;
}
if (child && child->applyBindingChange(newUnit, change))
return true;
}
return false;
}
void BindingPatchContext::reset(
const std::vector<QQmlRefPointer<QV4::ExecutableCompilationUnit>> &unitsToUnparent,
const std::vector<CompositeLevel> &internalUnits)
{
QQmlData *ddata = QQmlData::get(m_object);
const auto levelFor = [&internalUnits](
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldCu, int oldIndex) {
for (const CompositeLevel &level : internalUnits) {
if (level.oldCu == oldCu && level.objectIndex == oldIndex)
return level;
}
return CompositeLevel();
};
const QHash<QQmlAttachedPropertiesFunc, QObject *> *attachedProperties =
ddata->hasExtendedData() ? ddata->attachedProperties() : nullptr;
QObjectList children = m_object->children();
// Remove the children we shouldn't retire from the list. That is the ones that haven't been
// created by the relevant compilation units.
const auto newEnd = std::remove_if(children.begin(), children.end(), [&](QObject *child) {
const QQmlData *childDdata = QQmlData::get(child);
if (!childDdata)
return true;
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &cu = childDdata->compilationUnit;
if (!cu)
return true;
if (std::find(unitsToUnparent.begin(), unitsToUnparent.end(), cu) == unitsToUnparent.end())
return true;
if (!attachedProperties)
return false;
for (QObject *attached : *attachedProperties) {
if (attached == child)
return true;
}
return false;
});
children.erase(newEnd, children.end());
// Remove childrens' bindings before resetBindings() runs.
// resetBindings() clears list/default properties via list.clear(), which calls
// setParentItem(nullptr) and emits parentChanged. Any QProperty binding on a
// child that reads 'parent' would then fire with parent == null. Clearing those
// bindings first prevents a flood of related warnings.
for (QObject *child : children)
clearBindingsRecursive(child);
const CompositeLevel level = levelFor(unit, objectIndex);
resetBindings(unit, objectIndex, level.newCu, level.objectIndex);
for (QQmlVMEMetaObject *vmeMeta = ddata->hasVMEMetaObject
? static_cast<QQmlVMEMetaObject *>(QObjectPrivate::get(m_object)->metaObject)
: nullptr;
vmeMeta; vmeMeta = vmeMeta->parentVMEMetaObject()) {
const CompositeLevel level = levelFor(vmeMeta->compilationUnit(), vmeMeta->qmlObjectId());
resetBindings(vmeMeta->compilationUnit(), vmeMeta->qmlObjectId(), level.newCu,
level.objectIndex);
}
// Remove remaining composite signal handlers (all internal ones).
// External handlers were already detached by stashExternalState() and are invisible here.
// The object creator will recreate the internal handlers when it rebuilds the object.
while (QQmlBoundSignal *signalHandler = ddata->signalHandlers)
delete signalHandler;
// Objects from the old CU or composite-level CUs will be recreated by
// repopulateBindings. Unparent them so they don't interfere with the new objects.
// Attached property objects are reused across rebuilds.
for (QObject *child : children)
retireObject(child);
}
// Fully retire an old object that is being replaced by repopulateBindings.
// Recursively removes bindings from all descendants (unlinking expressions
// from context lists), then removes the subtree from the tree and schedules it
// for deletion. compilationUnit is intentionally left intact. The GC needs it.
void BindingPatchContext::retireObject(QObject *object)
{
// Remove from parent (in QtQuick "visual parent" or parentItem) via the meta property system.
// We must not assume any particular property to be the "parent" property here. That's what
// we have the ParentProperty classInfo for.
const QMetaObject *mo = object->metaObject();
if (const int classInfoIndex = mo->indexOfClassInfo("ParentProperty"); classInfoIndex >= 0) {
const QMetaClassInfo classInfo = mo->classInfo(classInfoIndex);
if (const int propertyIndex = mo->indexOfProperty(classInfo.value()); propertyIndex >= 0) {
const QMetaProperty property = mo->property(propertyIndex);
if ((!property.isResettable() || !property.reset(object)) && property.isWritable())
property.write(object, QVariant(property.metaType()));
}
}
// Unparent from QObject hierarchy so it no longer appears in
// parent->children(), then schedule deletion. The destructor will
// cascade-delete all QObject children (the recursive descendants).
// Mark the whole subtree as queued for deletion right away. Descendants may themselves be
// pending rebuild targets (e.g. inline component instances); they must not be rebuilt anymore.
QQml_setParent_noEvent(object, nullptr);
QQmlData::markAsDeleted(object);
object->deleteLater();
}
void BindingPatchContext::clearBindingsRecursive(QObject *object)
{
QQueue<QObject *> queue;
queue.enqueue(object);
while (!queue.isEmpty()) {
QObject *next = queue.dequeue();
queue.append(next->children());
QQmlData *ddata = QQmlData::get(next);
if (!ddata)
continue;
while (ddata->bindings)
QQmlPropertyPrivate::removeBinding(ddata->bindings);
// QProperty (BINDABLE) bindings live in the property's QPropertyBindingStorage,
// not in ddata->bindings. Remove them so they don't re-evaluate when
// setParentItem(nullptr) emits parentChanged during retireObject().
const QMetaObject *mo = next->metaObject();
for (int i = 0, n = mo->propertyCount(); i < n; ++i) {
if (!ddata->hasBindingBit(i))
continue;
const QMetaProperty prop = mo->property(i);
if (!prop.isBindable())
continue;
QUntypedBindable bindable = prop.bindable(next);
if (bindable.hasBinding())
bindable.takeBinding();
}
// Don't delete signal handlers right away since we might still have
// them in other objects "external" state. Disable them so that they
// don't fire anymore until this object is actually deleted.
for (QQmlBoundSignal *sig = ddata->signalHandlers; sig; sig = sig->m_nextSignal)
sig->setEnabled(false);
}
}
// Map the names of the properties that the new compilation unit binds on the given object
// to their bindings. repopulateBindings() will re-assign these, so resetting them before
// is redundant.
static ReboundBindings reboundBindings(
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &unit, int cuIndex)
{
ReboundBindings bindings;
if (!unit || cuIndex < 0 || cuIndex >= unit->objectCount())
return bindings;
const QV4::CompiledData::Object *obj = unit->objectAt(cuIndex);
for (auto binding = obj->bindingsBegin(), end = obj->bindingsEnd(); binding != end; ++binding)
bindings.insert(BindingPatchContext::targetPropertyName(unit, cuIndex, binding), binding);
return bindings;
}
// The new sub-object index for a group/attached property that the new CU rebinds with the
// same kind of binding, or -1 if the new CU doesn't rebind it.
static int reboundSubObjectIndex(const ReboundBindings &rebound,
const QString &name, QV4::CompiledData::Binding::Type type)
{
const QV4::CompiledData::Binding *newBinding = rebound.value(name);
return (newBinding && newBinding->type() == type) ? newBinding->value.objectIndex : -1;
}
void BindingPatchContext::resetBinding(
int objectIndex, const QV4::CompiledData::Binding *binding, const QString &name,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit,
const ReboundBindings &rebound)
{
QQmlProperty prop(m_object, prefix + name);
QQmlPropertyIndex propIdx = QQmlPropertyPrivate::propertyIndex(prop);
if (propIdx.coreIndex() < 0) {
// A generalized grouped property whose first chain part is an id (e.g. "someId.x")
// does not name a property of m_object; it targets an external object resolved by id.
// Reset the sub-bindings on that target.
Q_ASSERT(binding->isGroupedProperty());
if (BindingPatchContext *child = childContext(oldUnit, objectIndex, binding, nullptr)) {
child->resetBindings(
oldUnit, binding->value.objectIndex, newUnit,
reboundSubObjectIndex(rebound, name,
QV4::CompiledData::Binding::Type_GroupedProperty));
}
return;
}
const QMetaType type = prop.propertyMetaType();
const QMetaType::TypeFlags flags = type.flags();
if (flags.testFlag(QMetaType::IsQmlList)) {
// Lists need to always be cleared because they're generally additive.
// TODO: Handle ListPropertyAssignBehavior
QQmlListReference list = prop.read().value<QQmlListReference>();
if (list.clear())
return;
} else if (flags.testFlag(QMetaType::PointerToQObject) && binding->isGroupedProperty()) {
if (BindingPatchContext *child = childContext(oldUnit, objectIndex, binding, nullptr)) {
child->resetBindings(
oldUnit, binding->value.objectIndex, newUnit,
reboundSubObjectIndex(rebound, name,
QV4::CompiledData::Binding::Type_GroupedProperty));
}
return;
}
// Don't reset individual bindings that are re-bound anyway.
if (rebound.contains(name))
return;
if ((!prop.isResettable() || !prop.reset()) && prop.isWritable())
prop.write(QVariant(type));
}
void BindingPatchContext::resetBindings(
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit, int cuIndex,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit, int newCuIndex)
{
const QV4::CompiledData::Object *obj = oldUnit->objectAt(cuIndex);
const ReboundBindings rebound = reboundBindings(newUnit, newCuIndex);
for (auto binding = obj->bindingsBegin(), end = obj->bindingsEnd(); binding != end; ++binding) {
const QString name = targetPropertyName(oldUnit, cuIndex, binding);
if (name.isEmpty())
continue;
if (binding->isAttachedProperty()) {
// Recurse into existing attached objects to reset their bindings.
// The object creator will reuse them via qmlAttachedPropertiesObject().
if (!QQmlData::get(m_object)->hasExtendedData())
continue;
if (BindingPatchContext *attached = attachedContext(oldUnit, binding, nullptr)) {
attached->resetBindings(
oldUnit, binding->value.objectIndex, newUnit,
reboundSubObjectIndex(rebound, name,
QV4::CompiledData::Binding::Type_AttachedProperty));
}
continue;
}
if (oldUnit->bindingIsCustomParser(cuIndex, int(binding - obj->bindingsBegin())))
continue;
// Signal handlers are disconnected centrally.
if (!oldUnit->bindingIsSignalHandler(cuIndex, int(binding - obj->bindingsBegin())))
resetBinding(cuIndex, binding, name, oldUnit, newUnit, rebound);
}
}
// The name of the property a binding assigns, resolving the default property for unnamed bindings.
QString
BindingPatchContext::targetPropertyName(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &unit,
int objectIndex, const QV4::CompiledData::Binding *binding)
{
if (binding->propertyNameIndex != 0)
return unit->stringAt(binding->propertyNameIndex);
const QQmlPropertyCache::ConstPtr cache = unit->propertyCachesPtr()->at(objectIndex);
return cache ? cache->defaultPropertyName() : QString();
}
void BindingPatchContext::patchBinding(
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit,
const QV4::CompiledData::Change &change)
{
const QV4::CompiledData::Binding *newBinding =
newUnit->objectAt(change.objectIndex)->bindingTable() + change.index;
const QVariant newValue = literalBindingValue(newUnit, newBinding);
if (!newValue.isValid())
return; // Script binding: handled by function translation.
const QQmlProperty qProp(m_object,
prefix + targetPropertyName(newUnit, change.objectIndex, newBinding));
if (!qProp.isValid())
return;
QV4::ExecutionEngine *v4 = unit->engine;
const QMetaType metaType = qProp.propertyMetaType();
const QV4::CompiledData::Binding *oldBinding =
unit->objectAt(change.objectIndex)->bindingTable() + change.index;
const QVariant expectedOld =
coerceToPropertyType(v4, literalBindingValue(unit, oldBinding), metaType);
if (qProp.read() == expectedOld)
qProp.write(coerceToPropertyType(v4, newValue, metaType));
}
} // namespace QQmlPreview
QT_END_NAMESPACE