Support for QML module mapping

QUL uses module mapping for theming of QtQuick.Controls: during
code-generation the compiler is pointed to the Controls implementation
it should use. This is done by rewriting any import of QtQuick.Controls
with the given module name. The CMake build scripts will write a file
for each target to the directory "qml_module_mappings" in the build dir,
and those files will contain the mappings used.

Fixes: QTCREATORBUG-25356
Change-Id: I3f74897836dde7717b03bd6dffa46dcc0689ffdd
Reviewed-by: Fawzi Mohamed <fawzi.mohamed@qt.io>
This commit is contained in:
Erik Verbruggen
2021-03-17 11:38:33 +01:00
committed by Erik Verbruggen
parent 98dc428c69
commit 5ad724a3ac
17 changed files with 201 additions and 7 deletions

View File

@@ -221,19 +221,21 @@ bool Bind::visit(UiImport *ast)
if (ast->version)
version = ComponentVersion(ast->version->majorVersion, ast->version->minorVersion);
if (ast->importUri) {
if (auto importUri = ast->importUri) {
QVersionNumber qtVersion;
QString uri = toString(importUri);
if (ModelManagerInterface *model = ModelManagerInterface::instance()) {
ModelManagerInterface::ProjectInfo pInfo = model->projectInfoForPath(_doc->fileName());
qtVersion = QVersionNumber::fromString(pInfo.qtVersionString);
uri = pInfo.moduleMappings.value(uri, uri);
}
if (!version.isValid() && (!qtVersion.isNull() && qtVersion.majorVersion() < 6)) {
_diagnosticMessages->append(
errorMessage(ast, tr("package import requires a version number")));
}
const QString importId = ast->importId.toString();
ImportInfo import = ImportInfo::moduleImport(toString(ast->importUri), version,
importId, ast);
ImportInfo import = ImportInfo::moduleImport(uri, version, importId, ast);
if (_doc->language() == Dialect::Qml) {
const QString importStr = import.name() + importId;
if (ModelManagerInterface::instance()) {

View File

@@ -615,6 +615,7 @@ ModelManagerInterface::ProjectInfo ModelManagerInterface::projectInfoForPath(
res.applicationDirectories.append(pInfo.applicationDirectories);
for (const auto &importPath : pInfo.importPaths)
res.importPaths.maybeInsert(importPath);
res.moduleMappings.insert(pInfo.moduleMappings);
}
res.applicationDirectories = Utils::filteredUnique(res.applicationDirectories);
return res;

View File

@@ -72,6 +72,7 @@ public:
QStringList allResourceFiles;
QHash<QString, QString> resourceFileContents;
QStringList applicationDirectories;
QHash<QString, QString> moduleMappings; // E.g.: QtQuick.Controls -> MyProject.MyControls
// whether trying to run qmldump makes sense
bool tryQmlDump = false;

View File

@@ -685,7 +685,22 @@ void CMakeBuildSystem::updateProjectData()
const bool mergedHeaderPathsAndQmlImportPaths = kit()->value(
QtSupport::KitHasMergedHeaderPathsWithQmlImportPaths::id(), false).toBool();
QStringList extraHeaderPaths;
QList<QByteArray> moduleMappings;
for (const RawProjectPart &rpp : qAsConst(rpps)) {
FilePath moduleMapFile = cmakeBuildConfiguration()->buildDirectory()
.pathAppended("/qml_module_mappings/" + rpp.buildSystemTarget);
if (moduleMapFile.exists()) {
QFile mmf(moduleMapFile.toString());
if (mmf.open(QFile::ReadOnly)) {
QByteArray content = mmf.readAll();
auto lines = content.split('\n');
for (const auto &line : lines) {
if (!line.isEmpty())
moduleMappings.append(line.simplified());
}
}
}
if (mergedHeaderPathsAndQmlImportPaths) {
for (const auto &headerPath : rpp.headerPaths) {
if (headerPath.type == HeaderPathType::User)
@@ -693,7 +708,7 @@ void CMakeBuildSystem::updateProjectData()
}
}
}
updateQmlJSCodeModel(extraHeaderPaths);
updateQmlJSCodeModel(extraHeaderPaths, moduleMappings);
}
emit cmakeBuildConfiguration()->buildTypeChanged();
@@ -1187,8 +1202,10 @@ QList<ProjectExplorer::ExtraCompiler *> CMakeBuildSystem::findExtraCompilers()
return extraCompilers;
}
void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths)
void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths,
const QList<QByteArray> &moduleMappings)
{
qDebug()<<"cmake: module mappings:"<<moduleMappings;
QmlJS::ModelManagerInterface *modelManager = QmlJS::ModelManagerInterface::instance();
if (!modelManager)
@@ -1214,6 +1231,24 @@ void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths)
projectInfo.importPaths.maybeInsert(FilePath::fromString(extraHeaderPath),
QmlJS::Dialect::Qml);
for (const QByteArray &mm : moduleMappings) {
auto kvPair = mm.split('=');
if (kvPair.size() != 2)
continue;
QString from = QString::fromUtf8(kvPair.at(0).trimmed());
QString to = QString::fromUtf8(kvPair.at(1).trimmed());
if (!from.isEmpty() && !to.isEmpty() && from != to) {
// The QML code-model does not support sub-projects, so if there are multiple mappings for a single module,
// choose the shortest one.
if (projectInfo.moduleMappings.contains(from)) {
if (to.size() < projectInfo.moduleMappings.value(from).size())
projectInfo.moduleMappings.insert(from, to);
} else {
projectInfo.moduleMappings.insert(from, to);
}
}
}
project()->setProjectLanguage(ProjectExplorer::Constants::QMLJS_LANGUAGE_ID,
!projectInfo.sourceFiles.isEmpty());
modelManager->updateProjectInfo(projectInfo, p);

View File

@@ -140,7 +140,8 @@ private:
void updateProjectData();
void updateFallbackProjectData();
QList<ProjectExplorer::ExtraCompiler *> findExtraCompilers();
void updateQmlJSCodeModel(const QStringList &extraHeaderPaths);
void updateQmlJSCodeModel(const QStringList &extraHeaderPaths,
const QList<QByteArray> &moduleMappings);
void handleParsingSucceeded();
void handleParsingFailed(const QString &msg);

View File

@@ -0,0 +1,4 @@
import QtQuick 2.15
Item {
}

View File

@@ -0,0 +1,3 @@
module MyControls
import QtQuick
Oblong 1.0 Oblong.qml

View File

@@ -0,0 +1,4 @@
import QtQuick 2.15
Rect {
}

View File

@@ -0,0 +1,3 @@
module QtQuick.Controls
import QtQuick
Button 1.0 Button.qml

View File

@@ -0,0 +1,5 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
Item {
}

View File

@@ -61,18 +61,22 @@ private slots:
void importTypes_data();
void importTypes();
void moduleMapping_data();
void moduleMapping();
void initTestCase();
private:
QStringList m_basePaths;
};
void scanDir(const QString &dir)
void scanDirectory(const QString &dir)
{
QFutureInterface<void> result;
PathsAndLanguages paths;
paths.maybeInsert(Utils::FilePath::fromString(dir), Dialect::Qml);
ModelManagerInterface::importScan(result, ModelManagerInterface::workingCopy(), paths,
ModelManagerInterface::instance(), false);
QCoreApplication::processEvents();
ModelManagerInterface::instance()->test_joinAllThreads();
ViewerContext vCtx;
vCtx.paths.append(dir);
@@ -269,6 +273,7 @@ void tst_ImportCheck::importTypes()
modelManager->activateScan();
modelManager->updateSourceFiles(QStringList(qmlFile), false);
QCoreApplication::processEvents();
modelManager->test_joinAllThreads();
Snapshot snapshot = modelManager->newestSnapshot();
@@ -283,6 +288,7 @@ void tst_ImportCheck::importTypes()
return link();
};
getContext();
QCoreApplication::processEvents();
modelManager->test_joinAllThreads();
snapshot = modelManager->newestSnapshot();
doc = snapshot.document(qmlFile);
@@ -299,6 +305,102 @@ void tst_ImportCheck::importTypes()
QVERIFY(allFound);
}
typedef QHash<QString, QString> StrStrHash;
void tst_ImportCheck::moduleMapping_data()
{
QTest::addColumn<QString>("qmlFile");
QTest::addColumn<QString>("importPath");
QTest::addColumn<StrStrHash>("moduleMappings");
QTest::addColumn<QStringList>("expectedTypes");
QTest::addColumn<bool>("expectedResult");
QTest::newRow("check for plain QtQuick/Controls")
<< QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml")
<< QString(TESTSRCDIR "/moduleMapping")
<< StrStrHash()
<< QStringList({ "Item", "Button" })
<< true;
QTest::newRow("check that MyControls is not imported")
<< QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml")
<< QString(TESTSRCDIR "/moduleMapping")
<< StrStrHash()
<< QStringList({ "Item", "Oblong" })
<< false;
QTest::newRow("check that QtQuick controls cannot be found with a mapping")
<< QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml")
<< QString(TESTSRCDIR "/moduleMapping")
<< StrStrHash({ std::make_pair(QStringLiteral("QtQuick.Controls"), QStringLiteral("MyControls")) })
<< QStringList({ "Item", "Button" })
<< false;
QTest::newRow("check that custom controls can be found with a mapping")
<< QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml")
<< QString(TESTSRCDIR "/moduleMapping")
<< StrStrHash({ std::make_pair(QStringLiteral("QtQuick.Controls"), QStringLiteral("MyControls")) })
<< QStringList({ "Item", "Oblong" }) // item is in QtQuick, and should still be found, as only
// the QtQuick.Controls are redirected
<< true;
}
void tst_ImportCheck::moduleMapping()
{
QFETCH(QString, qmlFile);
QFETCH(QString, importPath);
QFETCH(StrStrHash, moduleMappings);
QFETCH(QStringList, expectedTypes);
QFETCH(bool, expectedResult);
// full reset
delete ModelManagerInterface::instance();
MyModelManager *modelManager = new MyModelManager;
ModelManagerInterface::ProjectInfo defaultProject;
defaultProject.importPaths = PathsAndLanguages();
QString qtQuickImportPath = QString(TESTSRCDIR "/importTypes/imports-QtQuick-qmldir-import");
defaultProject.importPaths.maybeInsert(Utils::FilePath::fromString(qtQuickImportPath), Dialect::Qml);
defaultProject.importPaths.maybeInsert(Utils::FilePath::fromString(importPath), Dialect::Qml);
defaultProject.moduleMappings = moduleMappings;
modelManager->setDefaultProject(defaultProject, nullptr);
modelManager->activateScan();
scanDirectory(importPath);
scanDirectory(qtQuickImportPath);
modelManager->updateSourceFiles(QStringList(qmlFile), false);
QCoreApplication::processEvents();
modelManager->test_joinAllThreads();
Snapshot snapshot = modelManager->newestSnapshot();
Document::Ptr doc = snapshot.document(qmlFile);
QVERIFY(!doc.isNull());
// It's unfortunate, but nowadays linking can trigger async module loads,
// so do it once to start the process, then do it again for real once the
// dependencies are available.
const auto getContext = [&]() {
Link link(snapshot, modelManager->completeVContext(modelManager->projectVContext(doc->language(), doc),doc),
modelManager->builtins(doc));
return link();
};
getContext();
QCoreApplication::processEvents();
modelManager->test_joinAllThreads();
snapshot = modelManager->newestSnapshot();
doc = snapshot.document(qmlFile);
ContextPtr context = getContext();
bool allFound = true;
for (const auto &expected : expectedTypes) {
if (!context->lookupType(doc.data(), QStringList(expected))) {
allFound = false;
qWarning() << "Type '" << expected << "' not found";
}
}
QVERIFY(allFound == expectedResult);
delete ModelManagerInterface::instance();
}
#ifdef MANUAL_IMPORT_SCANNER
int main(int argc, char *argv[])

View File

@@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.13)
project(test_project)
add_executable(test_exe test.cc test.qml)
file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/qml_module_mappings/test_exe" CONTENT "QtQuick.Controls=MyControls\n")

View File

@@ -0,0 +1,5 @@
import QtQuick 2.0
Item {
property int myproperty
}

View File

@@ -0,0 +1,3 @@
module MyControls
import QtQuick
Button 1.0 Button.qml

View File

@@ -0,0 +1,9 @@
This is a test for the module mapping feature used by Qt for MCUs.
Please add this source directory to the QML_IMPORT_PATH! A Qt for MCUs kit will do this automatically, but other kits
won't.
You can check that it works by going to test.qml, and "myproperty" should not be underligned as error. Without mapping,
the use of Button would resolve to QtQuick.Control's Button, which doesn't have that property. With the mapping, it
redirects to MyControls's Button which does have the property. You can verify this by control/command-clicking on the
property. This should take you to MyControls/Button.qml.

View File

@@ -0,0 +1 @@
int main() {}

View File

@@ -0,0 +1,8 @@
import QtQuick 2.0
import QtQuick.Controls 2.12
Item {
Button {
myproperty: 1
}
}