From 5ad724a3ac3456f46bc487e5cd68bdeb13db4dad Mon Sep 17 00:00:00 2001 From: Erik Verbruggen Date: Wed, 17 Mar 2021 11:38:33 +0100 Subject: [PATCH] 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 --- src/libs/qmljs/qmljsbind.cpp | 8 +- src/libs/qmljs/qmljsmodelmanagerinterface.cpp | 1 + src/libs/qmljs/qmljsmodelmanagerinterface.h | 1 + .../cmakeprojectmanager/cmakebuildsystem.cpp | 39 ++++++- .../cmakeprojectmanager/cmakebuildsystem.h | 3 +- .../moduleMapping/MyControls/Oblong.qml | 4 + .../moduleMapping/MyControls/qmldir | 3 + .../moduleMapping/QtQuick/Controls/Button.qml | 4 + .../moduleMapping/QtQuick/Controls/qmldir | 3 + .../moduleMapping/importQtQuick.qml | 5 + .../importscheck/tst_importscheck.cpp | 104 +++++++++++++++++- .../testprojects/modulemapping/CMakeLists.txt | 7 ++ .../modulemapping/MyControls/Button.qml | 5 + .../modulemapping/MyControls/qmldir | 3 + .../qml/testprojects/modulemapping/README.txt | 9 ++ .../qml/testprojects/modulemapping/test.cc | 1 + .../qml/testprojects/modulemapping/test.qml | 8 ++ 17 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/Oblong.qml create mode 100644 tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/qmldir create mode 100644 tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/Button.qml create mode 100644 tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/qmldir create mode 100644 tests/auto/qml/codemodel/importscheck/moduleMapping/importQtQuick.qml create mode 100644 tests/manual/qml/testprojects/modulemapping/CMakeLists.txt create mode 100644 tests/manual/qml/testprojects/modulemapping/MyControls/Button.qml create mode 100644 tests/manual/qml/testprojects/modulemapping/MyControls/qmldir create mode 100644 tests/manual/qml/testprojects/modulemapping/README.txt create mode 100644 tests/manual/qml/testprojects/modulemapping/test.cc create mode 100644 tests/manual/qml/testprojects/modulemapping/test.qml diff --git a/src/libs/qmljs/qmljsbind.cpp b/src/libs/qmljs/qmljsbind.cpp index d24b95319a6..9838a7dfc9f 100644 --- a/src/libs/qmljs/qmljsbind.cpp +++ b/src/libs/qmljs/qmljsbind.cpp @@ -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()) { diff --git a/src/libs/qmljs/qmljsmodelmanagerinterface.cpp b/src/libs/qmljs/qmljsmodelmanagerinterface.cpp index e1189754bd0..ffcabfc18b7 100644 --- a/src/libs/qmljs/qmljsmodelmanagerinterface.cpp +++ b/src/libs/qmljs/qmljsmodelmanagerinterface.cpp @@ -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; diff --git a/src/libs/qmljs/qmljsmodelmanagerinterface.h b/src/libs/qmljs/qmljsmodelmanagerinterface.h index 2dc00c3ed8b..f6142255469 100644 --- a/src/libs/qmljs/qmljsmodelmanagerinterface.h +++ b/src/libs/qmljs/qmljsmodelmanagerinterface.h @@ -72,6 +72,7 @@ public: QStringList allResourceFiles; QHash resourceFileContents; QStringList applicationDirectories; + QHash moduleMappings; // E.g.: QtQuick.Controls -> MyProject.MyControls // whether trying to run qmldump makes sense bool tryQmlDump = false; diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp index e8c3bd3700f..e306c0dc00c 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp @@ -685,7 +685,22 @@ void CMakeBuildSystem::updateProjectData() const bool mergedHeaderPathsAndQmlImportPaths = kit()->value( QtSupport::KitHasMergedHeaderPathsWithQmlImportPaths::id(), false).toBool(); QStringList extraHeaderPaths; + QList 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 CMakeBuildSystem::findExtraCompilers() return extraCompilers; } -void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths) +void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths, + const QList &moduleMappings) { + qDebug()<<"cmake: module mappings:"<setProjectLanguage(ProjectExplorer::Constants::QMLJS_LANGUAGE_ID, !projectInfo.sourceFiles.isEmpty()); modelManager->updateProjectInfo(projectInfo, p); diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h index 069321e5cfa..c1f3c8f5d75 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h @@ -140,7 +140,8 @@ private: void updateProjectData(); void updateFallbackProjectData(); QList findExtraCompilers(); - void updateQmlJSCodeModel(const QStringList &extraHeaderPaths); + void updateQmlJSCodeModel(const QStringList &extraHeaderPaths, + const QList &moduleMappings); void handleParsingSucceeded(); void handleParsingFailed(const QString &msg); diff --git a/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/Oblong.qml b/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/Oblong.qml new file mode 100644 index 00000000000..48aa963b300 --- /dev/null +++ b/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/Oblong.qml @@ -0,0 +1,4 @@ +import QtQuick 2.15 + +Item { +} diff --git a/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/qmldir b/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/qmldir new file mode 100644 index 00000000000..fb93ddae5f7 --- /dev/null +++ b/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/qmldir @@ -0,0 +1,3 @@ +module MyControls +import QtQuick +Oblong 1.0 Oblong.qml diff --git a/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/Button.qml b/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/Button.qml new file mode 100644 index 00000000000..ceea3cbd284 --- /dev/null +++ b/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/Button.qml @@ -0,0 +1,4 @@ +import QtQuick 2.15 + +Rect { +} diff --git a/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/qmldir b/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/qmldir new file mode 100644 index 00000000000..e18a13a3349 --- /dev/null +++ b/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/qmldir @@ -0,0 +1,3 @@ +module QtQuick.Controls +import QtQuick +Button 1.0 Button.qml diff --git a/tests/auto/qml/codemodel/importscheck/moduleMapping/importQtQuick.qml b/tests/auto/qml/codemodel/importscheck/moduleMapping/importQtQuick.qml new file mode 100644 index 00000000000..3bfee86997e --- /dev/null +++ b/tests/auto/qml/codemodel/importscheck/moduleMapping/importQtQuick.qml @@ -0,0 +1,5 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Item { +} diff --git a/tests/auto/qml/codemodel/importscheck/tst_importscheck.cpp b/tests/auto/qml/codemodel/importscheck/tst_importscheck.cpp index 6d7579daa86..f325338e4d7 100644 --- a/tests/auto/qml/codemodel/importscheck/tst_importscheck.cpp +++ b/tests/auto/qml/codemodel/importscheck/tst_importscheck.cpp @@ -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 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 StrStrHash; + +void tst_ImportCheck::moduleMapping_data() +{ + QTest::addColumn("qmlFile"); + QTest::addColumn("importPath"); + QTest::addColumn("moduleMappings"); + QTest::addColumn("expectedTypes"); + QTest::addColumn("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[]) diff --git a/tests/manual/qml/testprojects/modulemapping/CMakeLists.txt b/tests/manual/qml/testprojects/modulemapping/CMakeLists.txt new file mode 100644 index 00000000000..a138dd3f98a --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/CMakeLists.txt @@ -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") + diff --git a/tests/manual/qml/testprojects/modulemapping/MyControls/Button.qml b/tests/manual/qml/testprojects/modulemapping/MyControls/Button.qml new file mode 100644 index 00000000000..1ca99665a56 --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/MyControls/Button.qml @@ -0,0 +1,5 @@ +import QtQuick 2.0 + +Item { + property int myproperty +} diff --git a/tests/manual/qml/testprojects/modulemapping/MyControls/qmldir b/tests/manual/qml/testprojects/modulemapping/MyControls/qmldir new file mode 100644 index 00000000000..8ec6772e596 --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/MyControls/qmldir @@ -0,0 +1,3 @@ +module MyControls +import QtQuick +Button 1.0 Button.qml diff --git a/tests/manual/qml/testprojects/modulemapping/README.txt b/tests/manual/qml/testprojects/modulemapping/README.txt new file mode 100644 index 00000000000..50c8585bfd0 --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/README.txt @@ -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. diff --git a/tests/manual/qml/testprojects/modulemapping/test.cc b/tests/manual/qml/testprojects/modulemapping/test.cc new file mode 100644 index 00000000000..237c8ce1817 --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/test.cc @@ -0,0 +1 @@ +int main() {} diff --git a/tests/manual/qml/testprojects/modulemapping/test.qml b/tests/manual/qml/testprojects/modulemapping/test.qml new file mode 100644 index 00000000000..e30e0846ba8 --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/test.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.12 + +Item { + Button { + myproperty: 1 + } +}