QmlDesigner: Notify error for bases cycle

When a cyclic dependency occurs within the code model, the
synchronization process halts, leading to inconsistencies in type data
across components. This desynchronization can cause unexpected behavior
or errors during runtime. Currently, the cycle must be manually resolved
by the user to restore proper synchronization. Future improvements may
include automated cycle detection and resolution mechanisms.

Change-Id: I7365122fab1912b20e1ae34e7ee4fd7137fb637e
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Marco Bubke
2025-06-19 11:45:00 +02:00
parent ae881c4799
commit 3531157611
7 changed files with 50 additions and 3 deletions

View File

@@ -34,7 +34,7 @@ public:
using IsBasicId = std::true_type; using IsBasicId = std::true_type;
using DatabaseType = long long; using DatabaseType = long long;
constexpr explicit SourceId() = default; constexpr SourceId() = default;
static constexpr SourceId create(DirectoryPathId directoryPathId, FileNameId fileNameId) static constexpr SourceId create(DirectoryPathId directoryPathId, FileNameId fileNameId)
{ {

View File

@@ -132,6 +132,8 @@ struct ProjectStorage::Statements
"defaultPropertyId=propertyDeclarationId " "defaultPropertyId=propertyDeclarationId "
"WHERE t.typeId=?", "WHERE t.typeId=?",
database}; database};
mutable Sqlite::ReadStatement<2, 1> selectTypeNameAndSourceIdByTypeIdStatement{
"SELECT name, sourceId FROM types WHERE typeId=?", database};
mutable Sqlite::ReadStatement<5, 1> selectExportedTypesByTypeIdStatement{ mutable Sqlite::ReadStatement<5, 1> selectExportedTypesByTypeIdStatement{
"SELECT moduleId, typeId, name, majorVersion, minorVersion " "SELECT moduleId, typeId, name, majorVersion, minorVersion "
"FROM exportedTypeNames " "FROM exportedTypeNames "
@@ -4428,9 +4430,13 @@ void ProjectStorage::checkForPrototypeChainCycle(TypeId typeId) const
category(), category(),
keyValue("type id", typeId)}; keyValue("type id", typeId)};
auto callback = [=](TypeId currentTypeId) { auto callback = [&](TypeId currentTypeId) {
if (typeId == currentTypeId) if (typeId == currentTypeId) {
auto [name, sourceId] = s->selectTypeNameAndSourceIdByTypeIdStatement
.value<std::tuple<Utils::SmallString, SourceId>>(typeId);
errorNotifier->prototypeCycle(name, sourceId);
throw PrototypeChainCycle{}; throw PrototypeChainCycle{};
}
}; };
s->selectPrototypeAndExtensionIdsStatement.readCallback(callback, typeId); s->selectPrototypeAndExtensionIdsStatement.readCallback(callback, typeId);

View File

@@ -32,6 +32,7 @@ public:
= 0; = 0;
virtual void qmltypesFileMissing(QStringView qmltypesPath) = 0; virtual void qmltypesFileMissing(QStringView qmltypesPath) = 0;
virtual void prototypeCycle(Utils::SmallStringView typeName, SourceId typeSourceId) = 0;
protected: protected:
~ProjectStorageErrorNotifierInterface() = default; ~ProjectStorageErrorNotifierInterface() = default;

View File

@@ -89,4 +89,13 @@ void ProjectStorageErrorNotifier::qmltypesFileMissing(QStringView qmltypesPath)
qmltypesPath); qmltypesPath);
} }
void ProjectStorageErrorNotifier::prototypeCycle(Utils::SmallStringView typeName, SourceId typeSourceId)
{
const QString typeNameString{typeName};
logIssue(ProjectExplorer::Task::Error,
Tr::tr("Prototype cycle detected for type %1 in %2.").arg(typeNameString),
m_pathCache.sourcePath(typeSourceId));
}
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -28,6 +28,7 @@ public:
SourceId qmlDocumentSourceId, SourceId qmlDocumentSourceId,
SourceId qmldirSourceId) override; SourceId qmldirSourceId) override;
void qmltypesFileMissing(QStringView qmltypesPath) override; void qmltypesFileMissing(QStringView qmltypesPath) override;
void prototypeCycle(Utils::SmallStringView typeName, SourceId typeSourceId) override;
private: private:
PathCacheType &m_pathCache; PathCacheType &m_pathCache;

View File

@@ -33,4 +33,8 @@ public:
QmlDesigner::SourceId qmldirSourceId), QmlDesigner::SourceId qmldirSourceId),
(override)); (override));
MOCK_METHOD(void, qmltypesFileMissing, (QStringView qmltypesPath), (override)); MOCK_METHOD(void, qmltypesFileMissing, (QStringView qmltypesPath), (override));
MOCK_METHOD(void,
prototypeCycle,
(Utils::SmallStringView typeName, QmlDesigner::SourceId typeSourceId),
(override));
}; };

View File

@@ -4448,6 +4448,32 @@ TEST_F(ProjectStorage, throw_for_prototype_chain_cycles)
QmlDesigner::PrototypeChainCycle); QmlDesigner::PrototypeChainCycle);
} }
TEST_F(ProjectStorage, notifies_error_for_prototype_chain_cycles)
{
auto package{createSimpleSynchronizationPackage()};
package.types[1].prototype = Storage::Synchronization::ImportedType{"Object2"};
package.types.push_back(Storage::Synchronization::Type{
"QObject2",
Storage::Synchronization::ImportedType{"Item"},
Storage::Synchronization::ImportedType{},
TypeTraitsKind::Reference,
sourceId3,
{Storage::Synchronization::ExportedType{pathToModuleId, "Object2"},
Storage::Synchronization::ExportedType{pathToModuleId, "Obj2"}}});
package.imports.emplace_back(pathToModuleId, Storage::Version{}, sourceId2);
package.imports.emplace_back(qtQuickModuleId, Storage::Version{}, sourceId3);
package.imports.emplace_back(pathToModuleId, Storage::Version{}, sourceId3);
EXPECT_CALL(errorNotifierMock, prototypeCycle(Eq("QObject2"), sourceId3));
EXPECT_ANY_THROW(
storage.synchronize(SynchronizationPackage{package.imports,
package.types,
{sourceId1, sourceId2, sourceId3},
package.moduleDependencies,
package.updatedModuleDependencySourceIds}));
}
TEST_F(ProjectStorage, throw_for_extension_chain_cycles) TEST_F(ProjectStorage, throw_for_extension_chain_cycles)
{ {
auto package{createSimpleSynchronizationPackage()}; auto package{createSimpleSynchronizationPackage()};