QmlDesigner: Notify error for alias cycle

When a cyclic dependency is introduced through QML aliases, the code
model's synchronization mechanism is disrupted. This interruption
prevents the model from updating type information consistently across
components, resulting in stale or incorrect type data. Such
inconsistencies can lead to unpredictable behavior or runtime errors in
the application. This commit documents the issue and highlights the need
for user intervention to manually resolve the cycle.

Change-Id: I3c8283b0bbeba0634463e783ee18d5c99d8c919a
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Marco Bubke
2025-06-19 12:24:54 +02:00
parent 3531157611
commit 00df54b0be
6 changed files with 60 additions and 4 deletions

View File

@@ -786,6 +786,12 @@ struct ProjectStorage::Statements
"FROM propertyDeclarations "
"WHERE propertyDeclarationId=?1 LIMIT 1",
database};
mutable Sqlite::ReadStatement<2, 1> selectPropertyDeclarationNameAndTypeIdForPropertyDeclarationIdStatement{
"SELECT name, typeId "
"FROM propertyDeclarations "
"WHERE propertyDeclarationId=?1 "
"LIMIT 1",
database};
mutable Sqlite::ReadStatement<1, 1> selectSignalDeclarationNamesForTypeStatement{
"WITH RECURSIVE "
" prototypes(typeId) AS ( "
@@ -4447,9 +4453,17 @@ void ProjectStorage::checkForAliasChainCycle(PropertyDeclarationId propertyDecla
NanotraceHR::Tracer tracer{"check for alias chain cycle",
category(),
keyValue("property declaration id", propertyDeclarationId)};
auto callback = [=](PropertyDeclarationId currentPropertyDeclarationId) {
if (propertyDeclarationId == currentPropertyDeclarationId)
auto callback = [&](PropertyDeclarationId currentPropertyDeclarationId) {
if (propertyDeclarationId == currentPropertyDeclarationId) {
auto [propertyName, typeId] = s->selectPropertyDeclarationNameAndTypeIdForPropertyDeclarationIdStatement
.value<std::tuple<Utils::SmallString, TypeId>>(
propertyDeclarationId);
auto [typeName, sourceId] = s->selectTypeNameAndSourceIdByTypeIdStatement
.value<std::tuple<Utils::SmallString, SourceId>>(typeId);
errorNotifier->aliasCycle(typeName, propertyName, sourceId);
throw AliasChainCycle{};
}
};
s->selectPropertyDeclarationIdsForAliasChainStatement.readCallback(callback,

View File

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

View File

@@ -98,4 +98,18 @@ void ProjectStorageErrorNotifier::prototypeCycle(Utils::SmallStringView typeName
m_pathCache.sourcePath(typeSourceId));
}
void ProjectStorageErrorNotifier::aliasCycle(Utils::SmallStringView typeName,
Utils::SmallStringView propertyName,
SourceId typeSourceId)
{
const QString typeNameString{typeName};
const QString propertyNameString{propertyName};
logIssue(ProjectExplorer::Task::Error,
Tr::tr("Alias cycle detected for type %1 and property %2 in %3.")
.arg(typeNameString)
.arg(propertyNameString),
m_pathCache.sourcePath(typeSourceId));
}
} // namespace QmlDesigner

View File

@@ -29,6 +29,9 @@ public:
SourceId qmldirSourceId) override;
void qmltypesFileMissing(QStringView qmltypesPath) override;
void prototypeCycle(Utils::SmallStringView typeName, SourceId typeSourceId) override;
void aliasCycle(Utils::SmallStringView typeName,
Utils::SmallStringView propertyName,
SourceId typeSourceId) override;
private:
PathCacheType &m_pathCache;

View File

@@ -37,4 +37,10 @@ public:
prototypeCycle,
(Utils::SmallStringView typeName, QmlDesigner::SourceId typeSourceId),
(override));
MOCK_METHOD(void,
aliasCycle,
(Utils::SmallStringView typeName,
Utils::SmallStringView propertyName,
QmlDesigner::SourceId typeSourceId),
(override));
};

View File

@@ -4677,7 +4677,7 @@ TEST_F(ProjectStorage, update_aliases_after_change_property_to_alias)
"objects"))))));
}
TEST_F(ProjectStorage, check_for_proto_type_cycle_throws)
TEST_F(ProjectStorage, check_for_alias_type_cycle_throws)
{
auto package{createSynchronizationPackageWithRecursiveAliases()};
package.types[1].propertyDeclarations.clear();
@@ -4688,7 +4688,7 @@ TEST_F(ProjectStorage, check_for_proto_type_cycle_throws)
ASSERT_THROW(storage.synchronize(package), QmlDesigner::AliasChainCycle);
}
TEST_F(ProjectStorage, check_for_proto_type_cycle_after_update_throws)
TEST_F(ProjectStorage, check_for_alias_type_cycle_after_update_throws)
{
auto package{createSynchronizationPackageWithRecursiveAliases()};
storage.synchronize(package);
@@ -4702,6 +4702,21 @@ TEST_F(ProjectStorage, check_for_proto_type_cycle_after_update_throws)
QmlDesigner::AliasChainCycle);
}
TEST_F(ProjectStorage, check_for_alias_type_cycle_after_update_notifies_about_error)
{
auto package{createSynchronizationPackageWithRecursiveAliases()};
storage.synchronize(package);
package.types[1].propertyDeclarations.clear();
package.types[1].propertyDeclarations.push_back(Storage::Synchronization::PropertyDeclaration{
"objects", Storage::Synchronization::ImportedType{"AliasItem2"}, "objects"});
importsSourceId2.emplace_back(qtQuickModuleId, Storage::Version{}, sourceId2);
EXPECT_CALL(errorNotifierMock, aliasCycle(Eq("QObject"), Eq("objects"), sourceId2));
EXPECT_ANY_THROW(storage.synchronize(
SynchronizationPackage{importsSourceId2, {package.types[1]}, {sourceId2}}));
}
TEST_F(ProjectStorage, qualified_prototype)
{
auto package{createSimpleSynchronizationPackage()};