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;
};
updatePrototypeToNullStatement.readCallback(callback, &prototypeId);
updatePrototypeIdToNullStatement.readCallback(callback, &prototypeId);
}
void deleteType(TypeId typeId,
@@ -640,6 +640,7 @@ private:
}
updateTypePrototypeStatement.write(&prototype.typeId, &prototypeId, &prototypeNameId);
checkForPrototypeChainCycle(prototype.typeId);
}
}
@@ -1093,6 +1094,18 @@ private:
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)
{
if (Utils::visit([](auto &&typeName) -> bool { return typeName.name.isEmpty(); },
@@ -1106,6 +1119,7 @@ private:
throw TypeNameDoesNotExists{};
updatePrototypeStatement.write(&type.typeId, &prototypeId, &prototypeTypeNameId);
checkForPrototypeChainCycle(type.typeId);
}
}
@@ -1517,11 +1531,8 @@ public:
Initializer initializer;
ReadWriteStatement<1> upsertTypeStatement{
"INSERT INTO types(importId, name, accessSemantics, sourceId) VALUES(?1, ?2, "
"?3, nullif(?4, -1)) ON "
"CONFLICT DO UPDATE SET prototypeId=excluded.prototypeId, "
"accessSemantics=excluded.accessSemantics, sourceId=excluded.sourceId WHERE "
"prototypeId iS NOT excluded.prototypeId OR accessSemantics IS NOT "
"excluded.accessSemantics OR "
"?3, nullif(?4, -1)) ON CONFLICT DO UPDATE SET accessSemantics=excluded.accessSemantics, "
"sourceId=excluded.sourceId WHERE accessSemantics IS NOT excluded.accessSemantics OR "
"sourceId IS NOT excluded.sourceId RETURNING typeId",
database};
WriteStatement updatePrototypeStatement{
@@ -1819,13 +1830,21 @@ public:
ReadStatement<1> selectPropertyDeclarationIdStatement{
"SELECT propertyDeclarationId FROM propertyDeclarations WHERE propertyDeclarationId=?",
database};
ReadWriteStatement<3> updatePrototypeToNullStatement{
ReadWriteStatement<3> updatePrototypeIdToNullStatement{
"UPDATE types SET prototypeId=NULL WHERE prototypeId=?1 RETURNING "
"typeId, prototypeNameId, sourceId",
database};
ReadStatement<1> selectTypeIdStatement{"SELECT typeId FROM types WHERE typeId=?", database};
WriteStatement updateTypePrototypeStatement{
"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

View File

@@ -89,4 +89,10 @@ public:
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

View File

@@ -912,8 +912,15 @@ TEST_F(ProjectStorageSlowTest, SynchronizeTypesAddsNewTypesThrowsWithWrongExport
TEST_F(ProjectStorageSlowTest, SynchronizeTypesAddsNewTypesWithMissingImportAndExportedPrototypeName)
{
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}}});
types[1].prototype = Storage::ExportedType{"Object"};
types[1].prototype = Storage::ExportedType{"Object2"};
ASSERT_THROW(storage.synchronizeTypes(types, {sourceId1, sourceId2}),
QmlDesigner::TypeNameDoesNotExists);
@@ -3070,4 +3077,40 @@ TEST_F(ProjectStorageSlowTest, ChangePrototypeTypeNameThrowsForWrongNativeProtot
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