diff options
6 files changed, 170 insertions, 16 deletions
diff --git a/src/qml/jsruntime/qv4qmlcontext.cpp b/src/qml/jsruntime/qv4qmlcontext.cpp index ecf56e90c5..0fd7cdc1ff 100644 --- a/src/qml/jsruntime/qv4qmlcontext.cpp +++ b/src/qml/jsruntime/qv4qmlcontext.cpp @@ -632,8 +632,17 @@ static ReturnedValue revertObjectMethodLookup(Lookup *l, ExecutionEngine *engine return QQmlContextWrapper::resolveQmlContextPropertyLookupGetter(l, engine, base); } +// The object may be gone once it is being destroyed. Its name is not defined then, just like +// when resolving the name anew. +static ReturnedValue throwLostObjectReferenceError(Lookup *l, ExecutionEngine *engine) +{ + return engine->throwReferenceError( + engine->currentStackFrame->v4Function->compilationUnit->runtimeStrings[l->nameIndex] + ->toQString()); +} + template<typename Call> -ReturnedValue callWithScopeObject(ExecutionEngine *engine, Value *base, Call c) +ReturnedValue callWithScopeObject(Lookup *l, ExecutionEngine *engine, Value *base, Call c) { Scope scope(engine); Scoped<QmlContext> qmlContext(scope, engine->qmlContext()); @@ -641,11 +650,8 @@ ReturnedValue callWithScopeObject(ExecutionEngine *engine, Value *base, Call c) return QV4::Encode::undefined(); QObject *scopeObject = qmlContext->qmlScope(); - if (!scopeObject) - return QV4::Encode::undefined(); - - if (QQmlData::wasDeleted(scopeObject)) - return QV4::Encode::undefined(); + if (!scopeObject || QQmlData::wasDeleted(scopeObject)) + return throwLostObjectReferenceError(l, engine); ScopedValue obj(scope, QV4::QObjectWrapper::wrap(engine, scopeObject)); @@ -657,7 +663,7 @@ ReturnedValue callWithScopeObject(ExecutionEngine *engine, Value *base, Call c) ReturnedValue QQmlContextWrapper::lookupScopeObjectProperty(Lookup *l, ExecutionEngine *engine, Value *base) { - return callWithScopeObject(engine, base, [l, engine, base](const Value &obj) { + return callWithScopeObject(l, engine, base, [l, engine, base](const Value &obj) { const QObjectWrapper::Flags flags = l->forCall ? QObjectWrapper::NoFlag : QObjectWrapper::AttachMethods; @@ -669,7 +675,7 @@ ReturnedValue QQmlContextWrapper::lookupScopeObjectProperty(Lookup *l, Execution ReturnedValue QQmlContextWrapper::lookupScopeObjectMethod(Lookup *l, ExecutionEngine *engine, Value *base) { - return callWithScopeObject(engine, base, [l, engine, base](const Value &obj) { + return callWithScopeObject(l, engine, base, [l, engine, base](const Value &obj) { const QObjectWrapper::Flags flags = l->forCall ? QObjectWrapper::NoFlag : QObjectWrapper::AttachMethods; @@ -680,7 +686,7 @@ ReturnedValue QQmlContextWrapper::lookupScopeObjectMethod(Lookup *l, ExecutionEn } template<typename Call> -ReturnedValue callWithContextObject(ExecutionEngine *engine, Value *base, Call c) +ReturnedValue callWithContextObject(Lookup *l, ExecutionEngine *engine, Value *base, Call c) { Scope scope(engine); Scoped<QmlContext> qmlContext(scope, engine->qmlContext()); @@ -692,11 +698,8 @@ ReturnedValue callWithContextObject(ExecutionEngine *engine, Value *base, Call c return QV4::Encode::undefined(); QObject *contextObject = context->contextObject(); - if (!contextObject) - return QV4::Encode::undefined(); - - if (QQmlData::wasDeleted(contextObject)) - return QV4::Encode::undefined(); + if (!contextObject || QQmlData::wasDeleted(contextObject)) + return throwLostObjectReferenceError(l, engine); ScopedValue obj(scope, QV4::QObjectWrapper::wrap(engine, contextObject)); @@ -709,7 +712,7 @@ ReturnedValue callWithContextObject(ExecutionEngine *engine, Value *base, Call c ReturnedValue QQmlContextWrapper::lookupContextObjectProperty( Lookup *l, ExecutionEngine *engine, Value *base) { - return callWithContextObject(engine, base, [l, engine, base](const Value &obj) { + return callWithContextObject(l, engine, base, [l, engine, base](const Value &obj) { const QObjectWrapper::Flags flags = l->forCall ? QObjectWrapper::NoFlag : QObjectWrapper::AttachMethods; @@ -722,7 +725,7 @@ ReturnedValue QQmlContextWrapper::lookupContextObjectProperty( ReturnedValue QQmlContextWrapper::lookupContextObjectMethod( Lookup *l, ExecutionEngine *engine, Value *base) { - return callWithContextObject(engine, base, [l, engine, base](const Value &obj) { + return callWithContextObject(l, engine, base, [l, engine, base](const Value &obj) { const QObjectWrapper::Flags flags = l->forCall ? QObjectWrapper::NoFlag : QObjectWrapper::AttachMethods; diff --git a/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt b/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt index 009edbb34d..92831cb33d 100644 --- a/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt +++ b/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt @@ -248,6 +248,7 @@ set(qml_files listPropertyAsModel.qml listToString.qml listlength.qml + lostContextObject.qml markJSValue.qml markRecursive.qml math.qml diff --git a/tests/auto/qml/qmlcppcodegen/data/lostContextObject.qml b/tests/auto/qml/qmlcppcodegen/data/lostContextObject.qml new file mode 100644 index 0000000000..772058190c --- /dev/null +++ b/tests/auto/qml/qmlcppcodegen/data/lostContextObject.qml @@ -0,0 +1,28 @@ +pragma ComponentBehavior: Bound +import QtQml + +QtObject { + id: root + + property QtObject inner: component.createObject(root) + + property Component component: Component { + QtObject { + property int value: 5 + + function read(): int { + const v = value + console.warn("continued") + return v + } + + property QtObject child: QtObject { + function read(): int { + const v = value + console.warn("continued") + return v + } + } + } + } +} diff --git a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp index e8ced6cbea..4fa96c7f49 100644 --- a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp +++ b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp @@ -212,6 +212,8 @@ private slots: void listOfInlineComponent(); void listPropertyAsModel(); void listToString(); + void lostContextObject_data(); + void lostContextObject(); void lotsOfRegisters(); void math(); void mathMinMax(); @@ -4006,6 +4008,47 @@ void tst_QmlCppCodegen::listToString() std::unique_ptr<QObject> o(c.create()); } +void tst_QmlCppCodegen::lostContextObject_data() +{ + QTest::addColumn<bool>("onScopeObject"); + QTest::newRow("scope object") << true; + QTest::newRow("context object") << false; +} + +void tst_QmlCppCodegen::lostContextObject() +{ + QFETCH(bool, onScopeObject); + + QQmlEngine engine; + QQmlComponent component(&engine, QUrl(u"qrc:/qt/qml/TestTypes/lostContextObject.qml"_s)); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + std::unique_ptr<QObject> root(component.create()); + QVERIFY(root); + + QObject *inner = root->property("inner").value<QObject *>(); + QVERIFY(inner); + QObject *child = inner->property("child").value<QObject *>(); + QVERIFY(child); + + // The child outlives its context object, like a control's background. + child->setParent(root.get()); + + QJSValue read = engine.newQObject(onScopeObject ? inner : child).property(u"read"_s); + QVERIFY(read.isCallable()); + + // Populate the lookups. + QTest::ignoreMessage(QtWarningMsg, "continued"); + QCOMPARE(read.call().toInt(), 5); + + delete inner; + + // Reading the value of the lost object throws. The function must not continue. + QTest::failOnWarning("continued"); + const QJSValue result = read.call(); + QVERIFY(result.isError()); + QCOMPARE(result.errorType(), QJSValue::ReferenceError); +} + void tst_QmlCppCodegen::lotsOfRegisters() { QQmlEngine engine; diff --git a/tests/auto/qml/qqmlecmascript/data/deletedContextObjectProperty.qml b/tests/auto/qml/qqmlecmascript/data/deletedContextObjectProperty.qml new file mode 100644 index 0000000000..4315eea822 --- /dev/null +++ b/tests/auto/qml/qqmlecmascript/data/deletedContextObjectProperty.qml @@ -0,0 +1,22 @@ +import QtQml + +QtObject { + id: root + + property QtObject inner: component.createObject(root) + + property Component component: Component { + QtObject { + property int value: 5 + function method() { return 7 } + + function readValue() { return value } + function callMethod() { return method() } + + property QtObject child: QtObject { + function readValue() { return value } + function callMethod() { return method() } + } + } + } +} diff --git a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp index f773e8477d..c6b57b8cdd 100644 --- a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp +++ b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp @@ -451,6 +451,8 @@ private slots: void namedFunctionExpressionOwnNameIsShadowable(); void cuObjectIndex(); void vmeMetaObjectAccessors(); + void deletedContextObjectProperty_data(); + void deletedContextObjectProperty(); private: // static void propertyVarWeakRefCallback(v8::Persistent<v8::Value> object, void* parameter); @@ -10983,6 +10985,61 @@ void tst_qqmlecmascript::vmeMetaObjectAccessors() vme->parentVMEMetaObject(); } +void tst_qqmlecmascript::deletedContextObjectProperty_data() +{ + QTest::addColumn<bool>("onScopeObject"); + QTest::addColumn<QString>("function"); + QTest::addColumn<QVariant>("expected"); + QTest::addColumn<bool>("cached"); + + for (const bool cached : { false, true }) { + const char *suffix = cached ? " cached" : ""; + QTest::addRow("scope object property%s", suffix) + << true << u"readValue"_s << QVariant(5) << cached; + QTest::addRow("scope object method%s", suffix) + << true << u"callMethod"_s << QVariant(7) << cached; + QTest::addRow("context object property%s", suffix) + << false << u"readValue"_s << QVariant(5) << cached; + QTest::addRow("context object method%s", suffix) + << false << u"callMethod"_s << QVariant(7) << cached; + } +} + +void tst_qqmlecmascript::deletedContextObjectProperty() +{ + QFETCH(bool, onScopeObject); + QFETCH(QString, function); + QFETCH(QVariant, expected); + QFETCH(bool, cached); + + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("deletedContextObjectProperty.qml")); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + QScopedPointer<QObject> root(component.create()); + QVERIFY(root); + + QObject *inner = root->property("inner").value<QObject *>(); + QVERIFY(inner); + QObject *child = inner->property("child").value<QObject *>(); + QVERIFY(child); + + // Like a control's background, the child is not a QObject child of its context object. It is + // not marked as deleted along with it, and its functions can still run after the context has + // lost its context object. + child->setParent(root.data()); + + QJSValue fn = engine.newQObject(onScopeObject ? inner : child).property(function); + QVERIFY(fn.isCallable()); + if (cached) + QCOMPARE(fn.call().toVariant(), expected); + + // No matter whether the lookup was cached before, the name is not defined anymore. + QQmlData::markAsDeleted(inner); + const QJSValue result = fn.call(); + QVERIFY(result.isError()); + QCOMPARE(result.errorType(), QJSValue::ReferenceError); +} + QTEST_MAIN(tst_qqmlecmascript) #include "tst_qqmlecmascript.moc" |
