qmltc: support inline components from different qml files

Make qmltc aware that inline components can be shared between files such
that it does not complain about not finding them.
Typical usage are `MyOtherFile.MyInlineComponent {}` and
`ModuleName.MyOtherFile.MyInlineComponent {}` for an an inline
component called MyInlineComponent defined in MyOtherFile.qml, maybe
also in a module called ModuleName.

Make QQmlJSScope::findType() aware that inline components exists (and
potentially that they might also appear in namespaced types). They can
also be "imported" or reexported from basetypes and/or deeply nested in
some children scopes. Also make it public so that qqmljsimportvisitor
can use it when processing property types.
Added some tests testing both notations (with and without the qualifed
module name). Also add a test to see if there is no confusion between
the enums and the inline components (due to their very similar
notations).

Fixes: QTBUG-106592
Change-Id: I8f2d4790729902ffa664fd0eb1b7c3279af8ddca
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
This commit is contained in:
Sami Shalayel 2022-09-22 15:18:38 +02:00
parent 728270a0cb
commit bfdf1bf797
10 changed files with 177 additions and 8 deletions

View File

@ -604,7 +604,8 @@ void QQmlJSImportVisitor::processPropertyTypes()
auto property = type.scope->ownProperty(type.name);
if (auto propertyType = QQmlJSScope::findType(property.typeName(), m_rootScopeImports).scope) {
if (const auto propertyType =
QQmlJSScope::findType(property.typeName(), m_rootScopeImports).scope) {
property.setType(propertyType);
type.scope->addOwnProperty(property);
} else {

View File

@ -326,6 +326,43 @@ QQmlJSScope::findJSIdentifier(const QString &id) const
return std::optional<JavaScriptIdentifier>{};
}
static QQmlJSScope::ImportedScope<QQmlJSScope::ConstPtr>
qFindInlineComponents(QStringView typeName, const QQmlJSScope::ContextualTypes &contextualTypes)
{
const int separatorIndex = typeName.lastIndexOf(u'.');
// do not crash in typeName.sliced() when it starts or ends with an '.'.
if (separatorIndex < 1 || separatorIndex >= typeName.size() - 1)
return {};
const auto parentIt = contextualTypes.types.constFind(typeName.first(separatorIndex).toString());
if (parentIt == contextualTypes.types.constEnd())
return {};
auto inlineComponentParent = *parentIt;
// find the inline components using BFS, as inline components defined in childrens are also
// accessible from other qml documents. Same for inline components defined in a base class of
// the parent. Use BFS over DFS as the inline components are probably not deeply-nested.
QStringView inlineComponentName = typeName.sliced(separatorIndex + 1);
QQueue<QQmlJSScope::ConstPtr> candidatesForInlineComponents;
candidatesForInlineComponents.enqueue(inlineComponentParent.scope);
while (candidatesForInlineComponents.size()) {
QQmlJSScope::ConstPtr current = candidatesForInlineComponents.dequeue();
if (!current) // if some type was not resolved, ignore it instead of crashing
continue;
if (current->isInlineComponent() && current->inlineComponentName() == inlineComponentName) {
return { current, inlineComponentParent.revision };
}
// check alternatively the inline components at layer 1 in current and basetype, then at
// layer 2, etc...
candidatesForInlineComponents.append(current->childScopes());
if (const auto base = current->baseType())
candidatesForInlineComponents.enqueue(base);
}
return {};
}
QQmlJSScope::ImportedScope<QQmlJSScope::ConstPtr> QQmlJSScope::findType(
const QString &name, const QQmlJSScope::ContextualTypes &contextualTypes,
QSet<QString> *usedTypes)
@ -364,11 +401,15 @@ QQmlJSScope::ImportedScope<QQmlJSScope::ConstPtr> QQmlJSScope::findType(
break;
}
case ContextualTypes::QML: {
// TODO look for inline components with qmlFileName.InlineComponentName syntax
// look after inline components
const auto inlineComponent = qFindInlineComponents(name, contextualTypes);
if (inlineComponent.scope) {
useType();
return inlineComponent;
}
break;
}
}
return {};
}

View File

@ -614,7 +614,7 @@ public:
* If a type is found, then its name is inserted into usedTypes (when provided).
* If contextualTypes has mode INTERNAl, then namespace resolution for enums is
* done (eg for Qt::Alignment).
* TODO: If contextualTypes has mode QML, then inline component resolution is done
* If contextualTypes has mode QML, then inline component resolution is done
* ("qmlFileName.IC" is correctly resolved from qmlFileName).
*/
static ImportedScope<QQmlJSScope::ConstPtr> findType(const QString &name,
@ -625,7 +625,6 @@ private:
QQmlJSScope() = default;
QQmlJSScope(const QQmlJSScope &) = default;
QQmlJSScope &operator=(const QQmlJSScope &) = default;
static QTypeRevision resolveType(
const QQmlJSScope::Ptr &self, const ContextualTypes &contextualTypes,
QSet<QString> *usedTypes);

View File

@ -102,6 +102,7 @@ set(qml_sources
inlineComponents.qml
repeaterCrash.qml
aliases.qml
inlineComponentsFromDifferentFiles.qml
# support types:
DefaultPropertySingleChild.qml
@ -114,6 +115,8 @@ set(qml_sources
ComponentWithAlias1.qml
ComponentWithAlias2.qml
ComponentWithAlias3.qml
InlineComponentProvider.qml
InlineComponentReexporter.qml
badFile.qml
)

View File

@ -0,0 +1,24 @@
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
Item {
component IC1 : Item {
property string objName: "IC1"
}
component IC2: QtObject {
property string objName: "IC2"
}
enum MyEnum { Jumps, Over, The, Lazy, Dog }
Item {
component IC3: Item {
property string objName: "IC3"
}
}
}

View File

@ -0,0 +1,8 @@
import QtQuick
Item {
component IC100: InlineComponentProvider.IC1 {
objName: "IC100"
}
}

View File

@ -0,0 +1,18 @@
import QtQuick
import QmltcTests as MyModule
Item {
property MyModule.InlineComponentProvider.IC1 fromModule1
fromModule1: MyModule.InlineComponentProvider.IC1 {}
property var fromModule2: MyModule.InlineComponentProvider.IC1 {}
property InlineComponentProvider.IC1 fromOtherFile1: InlineComponentProvider.IC1 {}
property InlineComponentProvider.IC2 fromOtherFile2: InlineComponentProvider.IC2 {}
property InlineComponentProvider.IC3 fromOtherFile3: InlineComponentProvider.IC3 {}
property InlineComponentReexporter.IC100 reExported: InlineComponentReexporter.IC100 {}
property var looksLikeEnumIsEnum: InlineComponentProvider.Dog
property var looksLikeEnumIsInlineComponent: InlineComponentProvider.IC1 {}
}

View File

@ -77,6 +77,8 @@
#include "inlinecomponents.h"
#include "repeatercrash.h"
#include "aliases.h"
#include "inlinecomponentsfromdifferentfiles.h"
#include "testprivateproperty.h"
// Qt:
@ -2995,4 +2997,73 @@ void tst_qmltc::aliases()
QCOMPARE(fromComponent->property("aliasToOtherFile"), testString);
}
void tst_qmltc::inlineComponentsFromDifferentFiles()
{
// check that inline components can be imported from different files
QQmlEngine e;
PREPEND_NAMESPACE(inlineComponentsFromDifferentFiles) createdByQmltc(&e);
QQmlComponent component(&e);
component.loadUrl(QUrl("qrc:/qt/qml/QmltcTests/inlineComponentsFromDifferentFiles.qml"));
QVERIFY(!component.isError());
QScopedPointer<QObject> createdByComponent(component.create());
QCOMPARE(createdByQmltc.fromModule1()->objName(), u"IC1"_s);
QCOMPARE(createdByComponent->property("fromModule1")
.value<QObject *>()
->property("objName")
.toString(),
u"IC1"_s);
QCOMPARE(createdByQmltc.fromModule2().value<QObject *>()->property("objName").toString(),
u"IC1"_s);
QCOMPARE(createdByComponent->property("fromModule2")
.value<QObject *>()
->property("objName")
.toString(),
u"IC1"_s);
QCOMPARE(createdByQmltc.fromOtherFile1()->objName(), u"IC1"_s);
QCOMPARE(createdByComponent->property("fromOtherFile1")
.value<QObject *>()
->property("objName")
.toString(),
u"IC1"_s);
QCOMPARE(createdByQmltc.fromOtherFile2()->objName(), u"IC2"_s);
QCOMPARE(createdByComponent->property("fromOtherFile2")
.value<QObject *>()
->property("objName")
.toString(),
u"IC2"_s);
QCOMPARE(createdByQmltc.fromOtherFile3()->objName(), u"IC3"_s);
QCOMPARE(createdByComponent->property("fromOtherFile3")
.value<QObject *>()
->property("objName")
.toString(),
u"IC3"_s);
QCOMPARE(createdByQmltc.reExported()->objName(), u"IC100"_s);
QCOMPARE(createdByComponent->property("reExported")
.value<QObject *>()
->property("objName")
.toString(),
u"IC100"_s);
// test how good/bad inline components mix up with enums (they have very similar syntax)
// hard code enum values for better test readability
const int dog = 4;
QCOMPARE(createdByQmltc.looksLikeEnumIsEnum(), dog);
QCOMPARE(
createdByQmltc.looksLikeEnumIsInlineComponent().value<QObject *>()->property("objName"),
u"IC1"_s);
QCOMPARE(createdByComponent->property("looksLikeEnumIsEnum"), dog);
QCOMPARE(createdByComponent->property("looksLikeEnumIsInlineComponent")
.value<QObject *>()
->property("objName"),
u"IC1"_s);
}
QTEST_MAIN(tst_qmltc)

View File

@ -89,4 +89,5 @@ private slots:
void appendToQQmlListProperty();
void inlineComponents();
void aliases();
void inlineComponentsFromDifferentFiles();
};

View File

@ -188,7 +188,7 @@ bool QmltcVisitor::visit(QQmlJS::AST::UiObjectDefinition *object)
if (!QQmlJSImportVisitor::visit(object))
return false;
if (processingRoot) {
if (processingRoot || m_currentScope->isInlineComponent()) {
Q_ASSERT(rootScopeIsValid());
setRootFilePath();
}
@ -796,9 +796,12 @@ void QmltcVisitor::checkForNamingCollisionsWithCpp(const QQmlJSScope::ConstPtr &
// this information in QQmlJSMetaPropertyBinding currently
}
/*! \internal
* Sets the file paths for the document and the inline components roots.
*/
void QmltcVisitor::setRootFilePath()
{
const QString filePath = m_exportedRootScope->filePath();
const QString filePath = m_currentScope->filePath();
if (filePath.endsWith(u".h")) // assume the correct path is set
return;
Q_ASSERT(filePath.endsWith(u".qml"_s));
@ -816,7 +819,7 @@ void QmltcVisitor::setRootFilePath()
return;
}
// NB: get the file name to avoid prefixes
m_exportedRootScope->setFilePath(QFileInfo(*firstHeader).fileName());
m_currentScope->setFilePath(QFileInfo(*firstHeader).fileName());
}
QString QmltcVisitor::sourceDirectoryPath(const QString &path)