qml: Override the new Object::instanceOf hook to allow QML type checking

[ChangeLog][QtQml] The instanceof keyword in JavaScript has been
extended to work on QML types and instances. This means that you are now
able to use it to verify that a var is indeed the type you expect (e.g.
someVar instanceof Rectangle).

Note that one of the added tests revealed a slight shortcoming in the
QML type system (QTBUG-58477). For now, we should keep consistency and
work to address the problem universally in the future.

Change-Id: I7d9bf9b64cfd037908de1ae51b01065eacb95abe
Task-number: QTBUG-24799
Reviewed-by: Simon Hausmann <simon.hausmann@qt.io>
This commit is contained in:
Robin Burchell 2017-01-18 16:26:43 +01:00
parent 212ccd5962
commit 34ff6c40c1
14 changed files with 365 additions and 2 deletions

View File

@ -74,6 +74,19 @@ Note that QML makes the following modifications to native objects:
\li Locale-aware conversion functions are added to the \l Date and \l Number prototypes.
\endlist
In addition, QML also extends the behavior of the instanceof function to
allow for type checking against QML types. This means that you may use it to
verify that a variable is indeed the type you expect, for example:
\qml
var v = something();
if (!v instanceof Item) {
throw new TypeError("I need an Item type!");
}
...
\endqml
\section1 JavaScript Environment Restrictions

View File

@ -342,4 +342,44 @@ bool QmlTypeWrapper::isEqualTo(Managed *a, Managed *b)
return false;
}
ReturnedValue QmlTypeWrapper::instanceOf(const Object *typeObject, const Value &var)
{
Q_ASSERT(typeObject->as<QV4::QmlTypeWrapper>());
const QV4::QmlTypeWrapper *typeWrapper = static_cast<const QV4::QmlTypeWrapper *>(typeObject);
QV4::ExecutionEngine *engine = typeObject->internalClass()->engine;
QQmlEnginePrivate *qenginepriv = QQmlEnginePrivate::get(engine->qmlEngine());
// can only compare a QObject* against a QML type
const QObjectWrapper *wrapper = var.as<QObjectWrapper>();
if (!wrapper)
return engine->throwTypeError();
// in case the wrapper outlived the QObject*
const QObject *wrapperObject = wrapper->object();
if (!wrapperObject)
return engine->throwTypeError();
const int myTypeId = typeWrapper->d()->type->typeId();
QQmlMetaObject myQmlType;
if (myTypeId == 0) {
// we're a composite type; a composite type cannot be equal to a
// non-composite object instance (Rectangle{} is never an instance of
// CustomRectangle)
QQmlData *theirDData = QQmlData::get(wrapperObject, /*create=*/false);
Q_ASSERT(theirDData); // must exist, otherwise how do we have a QObjectWrapper for it?!
if (!theirDData->compilationUnit)
return Encode(false);
QQmlTypeData *td = qenginepriv->typeLoader.getType(typeWrapper->d()->type->sourceUrl());
CompiledData::CompilationUnit *cu = td->compilationUnit();
myQmlType = qenginepriv->metaObjectForType(cu->metaTypeId);
} else {
myQmlType = qenginepriv->metaObjectForType(myTypeId);
}
const QMetaObject *theirType = wrapperObject->metaObject();
return QV4::Encode(QQmlMetaObject::canConvert(theirType, myQmlType));
}
QT_END_NAMESPACE

View File

@ -103,7 +103,7 @@ struct Q_QML_EXPORT QmlTypeWrapper : Object
static bool put(Managed *m, String *name, const Value &value);
static PropertyAttributes query(const Managed *, String *name);
static bool isEqualTo(Managed *that, Managed *o);
static ReturnedValue instanceOf(const Object *typeObject, const Value &var);
};
}

View File

@ -8170,6 +8170,8 @@ void tst_qqmlecmascript::stringify_qtbug_50592()
QCOMPARE(obj->property("source").toString(), QString::fromLatin1("http://example.org/some_nonexistant_image.png"));
}
// Tests for the JS-only instanceof. Tests for the QML extensions for
// instanceof belong in tst_qqmllanguage!
void tst_qqmlecmascript::instanceof_data()
{
QTest::addColumn<QString>("setupCode");

View File

@ -0,0 +1,6 @@
import QtQuick 2.6
MouseArea {
}

View File

@ -0,0 +1,4 @@
import QtQuick 2.6
Rectangle {
}

View File

@ -0,0 +1,6 @@
import QtQuick 2.6
Rectangle {
property int somethingCustom: 0
}

View File

@ -0,0 +1,2 @@
CustomRectangle 1.0 CustomRectangle.qml
CustomMouseArea 1.0 CustomMouseArea.qml

View File

@ -0,0 +1,13 @@
import QtQml 2.0
QtObject {
id: qtobjectInstance
property Timer aTimer: Timer {
id: timerInstance
}
property Connections aConnections: Connections {
id: connectionsInstance
}
}

View File

@ -0,0 +1,13 @@
import QtQml 2.0 as QmlImport
QmlImport.QtObject {
id: qtobjectInstance
property QmlImport.Timer aTimer: QmlImport.Timer {
id: timerInstance
}
property QmlImport.Connections aConnections: QmlImport.Connections {
id: connectionsInstance
}
}

View File

@ -0,0 +1,14 @@
import QtQuick 2.0
Item {
id: itemInstance
Rectangle {
id: rectangleInstance
}
MouseArea {
id: mouseAreaInstance
}
}

View File

@ -0,0 +1,26 @@
import QtQuick 2.0
import "instanceOf"
Item {
id: itemInstance
Rectangle {
id: rectangleInstance
}
MouseArea {
id: mouseAreaInstance
}
CustomRectangle {
id: customRectangleInstance
}
CustomRectangleWithProp {
id: customRectangleWithPropInstance
}
CustomMouseArea {
id: customMouseAreaInstance
}
}

View File

@ -0,0 +1,27 @@
import QtQuick 2.0 as QuickImport
import "instanceOf" as CustomImport
QuickImport.Item {
id: itemInstance
QuickImport.Rectangle {
id: rectangleInstance
}
QuickImport.MouseArea {
id: mouseAreaInstance
}
CustomImport.CustomRectangle {
id: customRectangleInstance
}
CustomImport.CustomRectangleWithProp {
id: customRectangleWithPropInstance
}
CustomImport.CustomMouseArea {
id: customMouseAreaInstance
}
}

View File

@ -263,6 +263,9 @@ private slots:
void qmlTypeCanBeResolvedByName_data();
void qmlTypeCanBeResolvedByName();
void instanceof_data();
void instanceof();
private:
QQmlEngine engine;
QStringList defaultImportPathList;
@ -309,7 +312,7 @@ private:
if (!errorfile) { \
if (qgetenv("DEBUG") != "" && !component.errors().isEmpty()) \
qWarning() << "Unexpected Errors:" << component.errors(); \
QVERIFY(!component.isError()); \
QVERIFY2(!component.isError(), qPrintable(component.errorString())); \
QVERIFY(component.errors().isEmpty()); \
} else { \
DETERMINE_ERRORS(errorfile,expected,actual);\
@ -4338,6 +4341,200 @@ void tst_qqmllanguage::qmlTypeCanBeResolvedByName()
QVERIFY(!o.isNull());
}
// Tests for the QML-only extensions of instanceof. Tests for the regular JS
// instanceof belong in tst_qqmlecmascript!
void tst_qqmllanguage::instanceof_data()
{
QTest::addColumn<QUrl>("documentToTestIn");
QTest::addColumn<QVariant>("expectedValue");
// so the way this works is that the name of the test tag defines the test
// to run.
//
// the expectedValue is either a boolean true or false for whether the two
// operands are indeed an instanceof each other, or a string for the
// expected error message.
// assert that basic types don't convert to QObject
QTest::newRow("1 instanceof QtObject")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant("TypeError: Type error");
QTest::newRow("true instanceof QtObject")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant("TypeError: Type error");
QTest::newRow("\"foobar\" instanceof QtObject")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant("TypeError: Type error");
// assert that Managed don't either
QTest::newRow("new String(\"foobar\") instanceof QtObject")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant("TypeError: Type error");
QTest::newRow("new Object() instanceof QtObject")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant("TypeError: Type error");
QTest::newRow("new Date() instanceof QtObject")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant("TypeError: Type error");
// test that simple QtQml comparisons work
QTest::newRow("qtobjectInstance instanceof QtObject")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant(true);
QTest::newRow("qtobjectInstance instanceof Timer")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant(false);
QTest::newRow("timerInstance instanceof QtObject")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant(true);
QTest::newRow("timerInstance instanceof Timer")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant(true);
QTest::newRow("connectionsInstance instanceof QtObject")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant(true);
QTest::newRow("connectionsInstance instanceof Timer")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant(false);
QTest::newRow("connectionsInstance instanceof Connections")
<< testFileUrl("instanceof_qtqml.qml")
<< QVariant(true);
// make sure they still work when imported with a qualifier
QTest::newRow("qtobjectInstance instanceof QmlImport.QtObject")
<< testFileUrl("instanceof_qtqml_qualified.qml")
<< QVariant(true);
QTest::newRow("qtobjectInstance instanceof QmlImport.Timer")
<< testFileUrl("instanceof_qtqml_qualified.qml")
<< QVariant(false);
QTest::newRow("timerInstance instanceof QmlImport.QtObject")
<< testFileUrl("instanceof_qtqml_qualified.qml")
<< QVariant(true);
QTest::newRow("timerInstance instanceof QmlImport.Timer")
<< testFileUrl("instanceof_qtqml_qualified.qml")
<< QVariant(true);
QTest::newRow("connectionsInstance instanceof QmlImport.QtObject")
<< testFileUrl("instanceof_qtqml_qualified.qml")
<< QVariant(true);
QTest::newRow("connectionsInstance instanceof QmlImport.Timer")
<< testFileUrl("instanceof_qtqml_qualified.qml")
<< QVariant(false);
QTest::newRow("connectionsInstance instanceof QmlImport.Connections")
<< testFileUrl("instanceof_qtqml_qualified.qml")
<< QVariant(true);
// test that Quick C++ types work ok
QTest::newRow("itemInstance instanceof QtObject")
<< testFileUrl("instanceof_qtquick.qml")
<< QVariant(true);
QTest::newRow("itemInstance instanceof Timer")
<< testFileUrl("instanceof_qtquick.qml")
<< QVariant(false);
QTest::newRow("itemInstance instanceof Rectangle")
<< testFileUrl("instanceof_qtquick.qml")
<< QVariant(false);
QTest::newRow("rectangleInstance instanceof Item")
<< testFileUrl("instanceof_qtquick.qml")
<< QVariant(true);
QTest::newRow("rectangleInstance instanceof Rectangle")
<< testFileUrl("instanceof_qtquick.qml")
<< QVariant(true);
QTest::newRow("rectangleInstance instanceof MouseArea")
<< testFileUrl("instanceof_qtquick.qml")
<< QVariant(false);
QTest::newRow("mouseAreaInstance instanceof Item")
<< testFileUrl("instanceof_qtquick.qml")
<< QVariant(true);
QTest::newRow("mouseAreaInstance instanceof Rectangle")
<< testFileUrl("instanceof_qtquick.qml")
<< QVariant(false);
QTest::newRow("mouseAreaInstance instanceof MouseArea")
<< testFileUrl("instanceof_qtquick.qml")
<< QVariant(true);
// test that unqualified quick composite types work ok
QTest::newRow("rectangleInstance instanceof CustomRectangle")
<< testFileUrl("instanceof_qtquick_composite.qml")
<< QVariant(false);
QTest::newRow("customRectangleInstance instanceof Rectangle")
<< testFileUrl("instanceof_qtquick_composite.qml")
<< QVariant(true);
QTest::newRow("customRectangleInstance instanceof Item")
<< testFileUrl("instanceof_qtquick_composite.qml")
<< QVariant(true);
QTest::newRow("customRectangleWithPropInstance instanceof CustomRectangleWithProp")
<< testFileUrl("instanceof_qtquick_composite.qml")
<< QVariant(true);
QTest::newRow("customRectangleWithPropInstance instanceof CustomRectangle")
<< testFileUrl("instanceof_qtquick_composite.qml")
<< QVariant(false); // ### XXX: QTBUG-58477
QTest::newRow("customRectangleWithPropInstance instanceof Rectangle")
<< testFileUrl("instanceof_qtquick_composite.qml")
<< QVariant(true);
QTest::newRow("customRectangleInstance instanceof MouseArea")
<< testFileUrl("instanceof_qtquick_composite.qml")
<< QVariant(false);
QTest::newRow("customMouseAreaInstance instanceof MouseArea")
<< testFileUrl("instanceof_qtquick_composite.qml")
<< QVariant(true);
// test that they still work when qualified
QTest::newRow("rectangleInstance instanceof CustomImport.CustomRectangle")
<< testFileUrl("instanceof_qtquick_composite_qualified.qml")
<< QVariant(false);
QTest::newRow("customRectangleInstance instanceof QuickImport.Rectangle")
<< testFileUrl("instanceof_qtquick_composite_qualified.qml")
<< QVariant(true);
QTest::newRow("customRectangleInstance instanceof QuickImport.Item")
<< testFileUrl("instanceof_qtquick_composite_qualified.qml")
<< QVariant(true);
QTest::newRow("customRectangleWithPropInstance instanceof CustomImport.CustomRectangleWithProp")
<< testFileUrl("instanceof_qtquick_composite_qualified.qml")
<< QVariant(true);
QTest::newRow("customRectangleWithPropInstance instanceof CustomImport.CustomRectangle")
<< testFileUrl("instanceof_qtquick_composite_qualified.qml")
<< QVariant(false); // ### XXX: QTBUG-58477
QTest::newRow("customRectangleWithPropInstance instanceof QuickImport.Rectangle")
<< testFileUrl("instanceof_qtquick_composite_qualified.qml")
<< QVariant(true);
QTest::newRow("customRectangleInstance instanceof QuickImport.MouseArea")
<< testFileUrl("instanceof_qtquick_composite_qualified.qml")
<< QVariant(false);
QTest::newRow("customMouseAreaInstance instanceof QuickImport.MouseArea")
<< testFileUrl("instanceof_qtquick_composite_qualified.qml")
<< QVariant(true);
}
void tst_qqmllanguage::instanceof()
{
QFETCH(QUrl, documentToTestIn);
QFETCH(QVariant, expectedValue);
QQmlEngine engine;
QQmlComponent component(&engine, documentToTestIn);
VERIFY_ERRORS(0);
QScopedPointer<QObject> o(component.create());
QVERIFY(o != 0);
QQmlExpression expr(engine.contextForObject(o.data()), 0, QString::fromLatin1(QTest::currentDataTag()));
QVariant ret = expr.evaluate();
if (expectedValue.type() == QVariant::Bool) {
// no error expected
QVERIFY2(!expr.hasError(), qPrintable(expr.error().description()));
bool returnValue = ret.toBool();
if (QTest::currentDataTag() == QLatin1String("customRectangleWithPropInstance instanceof CustomRectangle") ||
QTest::currentDataTag() == QLatin1String("customRectangleWithPropInstance instanceof CustomImport.CustomRectangle"))
QEXPECT_FAIL("", "QTBUG-58477: QML type rules are a little lax", Continue);
QCOMPARE(returnValue, expectedValue.toBool());
} else {
QVERIFY(expr.hasError());
QCOMPARE(expr.error().description(), expectedValue.toString());
}
}
QTEST_MAIN(tst_qqmllanguage)
#include "tst_qqmllanguage.moc"