QmlDesigner: Prevent prototype chains cycles

A cycle would lead to an endless loop. So we throw an exception for
synchronization. Maybe we can add later more information to user so
he can easily resolve the error.

Task-number: QDS-4457
Change-Id: I83092ccdff030a610942c155571a0bfa899e808c
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
Marco Bubke
2021-07-06 15:45:10 +02:00
parent a8d6bb06df
commit a30775a19e
3 changed files with 76 additions and 8 deletions

View File

@@ -555,7 +555,7 @@ private:
return Sqlite::CallbackControl::Continue; return Sqlite::CallbackControl::Continue;
}; };
updatePrototypeToNullStatement.readCallback(callback, &prototypeId); updatePrototypeIdToNullStatement.readCallback(callback, &prototypeId);
} }
void deleteType(TypeId typeId, void deleteType(TypeId typeId,
@@ -640,6 +640,7 @@ private:
} }
updateTypePrototypeStatement.write(&prototype.typeId, &prototypeId, &prototypeNameId); updateTypePrototypeStatement.write(&prototype.typeId, &prototypeId, &prototypeNameId);
checkForPrototypeChainCycle(prototype.typeId);
} }
} }
@@ -1093,6 +1094,18 @@ private:
synchronizeEnumerationDeclarations(typeId, type.enumerationDeclarations); synchronizeEnumerationDeclarations(typeId, type.enumerationDeclarations);
} }
void checkForPrototypeChainCycle(TypeId typeId)
{
auto callback = [=](long long currentTypeId) {
if (typeId == TypeId{currentTypeId})
throw PrototypeChainCycle{};
return Sqlite::CallbackControl::Continue;
};
selectTypeIdsForPrototypeChainIdStatement.readCallback(callback, &typeId);
}
void syncPrototypes(Storage::Type &type) void syncPrototypes(Storage::Type &type)
{ {
if (Utils::visit([](auto &&typeName) -> bool { return typeName.name.isEmpty(); }, if (Utils::visit([](auto &&typeName) -> bool { return typeName.name.isEmpty(); },
@@ -1106,6 +1119,7 @@ private:
throw TypeNameDoesNotExists{}; throw TypeNameDoesNotExists{};
updatePrototypeStatement.write(&type.typeId, &prototypeId, &prototypeTypeNameId); updatePrototypeStatement.write(&type.typeId, &prototypeId, &prototypeTypeNameId);
checkForPrototypeChainCycle(type.typeId);
} }
} }
@@ -1517,11 +1531,8 @@ public:
Initializer initializer; Initializer initializer;
ReadWriteStatement<1> upsertTypeStatement{ ReadWriteStatement<1> upsertTypeStatement{
"INSERT INTO types(importId, name, accessSemantics, sourceId) VALUES(?1, ?2, " "INSERT INTO types(importId, name, accessSemantics, sourceId) VALUES(?1, ?2, "
"?3, nullif(?4, -1)) ON " "?3, nullif(?4, -1)) ON CONFLICT DO UPDATE SET accessSemantics=excluded.accessSemantics, "
"CONFLICT DO UPDATE SET prototypeId=excluded.prototypeId, " "sourceId=excluded.sourceId WHERE accessSemantics IS NOT excluded.accessSemantics OR "
"accessSemantics=excluded.accessSemantics, sourceId=excluded.sourceId WHERE "
"prototypeId iS NOT excluded.prototypeId OR accessSemantics IS NOT "
"excluded.accessSemantics OR "
"sourceId IS NOT excluded.sourceId RETURNING typeId", "sourceId IS NOT excluded.sourceId RETURNING typeId",
database}; database};
WriteStatement updatePrototypeStatement{ WriteStatement updatePrototypeStatement{
@@ -1819,13 +1830,21 @@ public:
ReadStatement<1> selectPropertyDeclarationIdStatement{ ReadStatement<1> selectPropertyDeclarationIdStatement{
"SELECT propertyDeclarationId FROM propertyDeclarations WHERE propertyDeclarationId=?", "SELECT propertyDeclarationId FROM propertyDeclarations WHERE propertyDeclarationId=?",
database}; database};
ReadWriteStatement<3> updatePrototypeToNullStatement{ ReadWriteStatement<3> updatePrototypeIdToNullStatement{
"UPDATE types SET prototypeId=NULL WHERE prototypeId=?1 RETURNING " "UPDATE types SET prototypeId=NULL WHERE prototypeId=?1 RETURNING "
"typeId, prototypeNameId, sourceId", "typeId, prototypeNameId, sourceId",
database}; database};
ReadStatement<1> selectTypeIdStatement{"SELECT typeId FROM types WHERE typeId=?", database}; ReadStatement<1> selectTypeIdStatement{"SELECT typeId FROM types WHERE typeId=?", database};
WriteStatement updateTypePrototypeStatement{ WriteStatement updateTypePrototypeStatement{
"UPDATE types SET prototypeId=?2, prototypeNameId=?3 WHERE typeId=?1", database}; "UPDATE types SET prototypeId=?2, prototypeNameId=?3 WHERE typeId=?1", database};
mutable ReadStatement<1> selectTypeIdsForPrototypeChainIdStatement{
"WITH RECURSIVE "
" prototypes(typeId) AS ("
" SELECT prototypeId FROM types WHERE typeId=? "
" UNION ALL "
" SELECT prototypeId FROM types JOIN prototypes USING(typeId)) "
"SELECT typeId FROM prototypes",
database};
}; };
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -89,4 +89,10 @@ public:
const char *what() const noexcept override { return "The property name does not exist!"; } const char *what() const noexcept override { return "The property name does not exist!"; }
}; };
class PrototypeChainCycle : std::exception
{
public:
const char *what() const noexcept override { return "There is a prototype chain cycle!"; }
};
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -912,8 +912,15 @@ TEST_F(ProjectStorageSlowTest, SynchronizeTypesAddsNewTypesThrowsWithWrongExport
TEST_F(ProjectStorageSlowTest, SynchronizeTypesAddsNewTypesWithMissingImportAndExportedPrototypeName) TEST_F(ProjectStorageSlowTest, SynchronizeTypesAddsNewTypesWithMissingImportAndExportedPrototypeName)
{ {
Storage::Types types{createTypes()}; Storage::Types types{createTypes()};
types.push_back(Storage::Type{importId3,
"QObject2",
Storage::NativeType{},
TypeAccessSemantics::Reference,
sourceId4,
importIds,
{Storage::ExportedType{"Object2"}, Storage::ExportedType{"Obj2"}}});
storage.synchronizeDocuments({Storage::Document{sourceId1, {importId1}}}); storage.synchronizeDocuments({Storage::Document{sourceId1, {importId1}}});
types[1].prototype = Storage::ExportedType{"Object"}; types[1].prototype = Storage::ExportedType{"Object2"};
ASSERT_THROW(storage.synchronizeTypes(types, {sourceId1, sourceId2}), ASSERT_THROW(storage.synchronizeTypes(types, {sourceId1, sourceId2}),
QmlDesigner::TypeNameDoesNotExists); QmlDesigner::TypeNameDoesNotExists);
@@ -3070,4 +3077,40 @@ TEST_F(ProjectStorageSlowTest, ChangePrototypeTypeNameThrowsForWrongNativeProtot
QmlDesigner::TypeNameDoesNotExists); QmlDesigner::TypeNameDoesNotExists);
} }
TEST_F(ProjectStorageSlowTest, ThrowForPrototypeChainCycles)
{
Storage::Types types{createTypes()};
types[1].prototype = Storage::ExportedType{"Object2"};
types.push_back(Storage::Type{importId3,
"QObject2",
Storage::ExportedType{"Item"},
TypeAccessSemantics::Reference,
sourceId3,
importIds,
{Storage::ExportedType{"Object2"}, Storage::ExportedType{"Obj2"}}});
ASSERT_THROW(storage.synchronizeTypes(types, {sourceId1, sourceId2, sourceId3}),
QmlDesigner::PrototypeChainCycle);
}
TEST_F(ProjectStorageSlowTest, ThrowForTypeIdAndPrototypeIdAreTheSame)
{
Storage::Types types{createTypes()};
types[1].prototype = Storage::ExportedType{"Object"};
ASSERT_THROW(storage.synchronizeTypes(types, {sourceId1, sourceId2}),
QmlDesigner::PrototypeChainCycle);
}
TEST_F(ProjectStorageSlowTest, ThrowForTypeIdAndPrototypeIdAreTheSameForRelinking)
{
Storage::Types types{createTypes()};
types[0].propertyDeclarations[0].typeName = Storage::ExportedType{"Object"};
types[0].prototype = Storage::ExportedType{"Object"};
storage.synchronizeTypes(types, {sourceId1, sourceId2});
types[1].prototype = Storage::ExportedType{"Item"};
types[1].typeName = "QObject2";
ASSERT_THROW(storage.synchronizeTypes({types[1]}, {sourceId2}), QmlDesigner::PrototypeChainCycle);
}
} // namespace } // namespace